diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index bdd3f55..5c7aa8e 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -7400,6 +7400,111 @@ bruker. Begge containere startet rent. --- +## ADR-075: Order of Merit -- eclectic-aggregering på tvers av lenkede turneringer ("Steg 2" av gjenstår-listen) (2026-08-15) + +Foranledning: andre av de tre gjenstående OOM-punktene fra ADR-043, se +ADR-074 sin innledning for hele bakgrunnen/bekreftelsen. Eclectic- +aggregering ("sesong-drømmerunde": beste resultat PER HULL på tvers av +ALLE lenkede turneringer, kun `result_type IN ('stableford','gross', +'net')`) var forberedt i skjemaet (`aggregation_mode='eclectic'` fantes +som verdi) men eksplisitt avvist i `order_of_merit_leaderboard` ("ikke +støttet ennå"). + +**Samme-bane-håndheving** (app-laget, ingen migrasjon): eclectic- +aggregering på tvers av turneringer er meningsløs hvis turneringene ikke +er spilt på samme bane (ulike hull-oppsett/par/stroke index). Håndhevet +på TO steder, samme "fail loudly"-filosofi som ADR-068/ADR-019 +Beslutning C: +- `link_tournament_to_order_of_merit`: avviser lenking av en turnering + hvis OOM-en allerede er `aggregation_mode='eclectic'` OG den nye + turneringens bane avviker fra en allerede lenket turnerings bane. +- `update_order_of_merit`: avviser å endre `aggregation_mode` TIL + `'eclectic'` hvis turneringer med ulik bane allerede er lenket. + +Ny `_validate_same_course_for_eclectic()` + `_course_id_for_tournament()` +(samme `LIMIT 1`-antakelse på `tournament_round.course_id` som +`_attach_eclectic_totals` allerede gjorde -- én bane per turnering, +udiskutert forutsetning i hele kodebasen). + +**Datainnsamling** (ny `_compute_eclectic_oom_values`, gjenbruker +`eclectic_best_per_hole()` fra `handicap_engine.py` HELT UENDRET -- +motoren viste seg, som antatt i planen, kildeuavhengig nok til å +fungere identisk for "flere runder i én turnering" (ADR-068) og "flere +turneringer i én OOM" (dette steget), kun datainnsamlingen foran den er +forskjellig): tilpasset variant av `_attach_eclectic_totals` sin +`tournament_round_hole`-spørring, filtrert på `tr.tournament_id = +ANY($linked_ids)` i stedet for én turnering. Nøkkelforskjell fra +innad-i-turnering-varianten: keyed på `player_id` (ikke +`tournament_participant_id`, som er ulik per turnering for samme +spiller), og kilde-indeksen er sammensatt av (turnering-id, runde- +sekvens) siden `sequence` kun er unik INNENFOR én turnering. Returnerer +også `tournament_count_by_player` -- antall DISTINKTE turneringer som +faktisk bidro minst ett hull til spillerens sluttsum, brukt som +`results_available` for `min_results_required`-sjekken (rå `len(values)` +ville alltid vært 1 i eclectic-modus, meningsløst som eligibility-mål). + +`_player_eligibility` sin signatur endret fra å ta `raw_values: list` +til å ta `results_available: int` direkte -- frikobler "hvor mange +resultater talte" fra "de faktiske verdiene", nødvendig fordi eclectic- +modusens tall ikke lenger kommer fra `len(raw_values)`. + +**Leaderboard**: `order_of_merit_leaderboard` sin tidligere eksplisitte +avvisning for `aggregation_mode == 'eclectic'` erstattet med faktisk +beregning -- kun for `kind='player'` (kind='team' + eclectic er allerede +sperret ved opprettelse, uendret fra ADR-043). + +**Frontend**: "Eclectic"-valget lagt til begge steder +`aggregation_mode` settes, gated identisk til (og speilende) server- +sidens regel (`kind==='player' && result_type IN +('stableford','gross','net')`) slik at UI-et aldri tilbyr et valg +serveren uansett ville avvist: +- `order-of-merit-list.tsx` (opprettelsesskjema): ny `eclecticAllowed()`- + helper, tredje `SegmentButton` vist betinget, PLUSS auto-reset til + `'sum'` hvis brukeren endrer `kind`/`resultType` ETTER å ha valgt + Eclectic slik at den ikke lenger er gyldig (kind/resultType er fritt + redigerbare før opprettelse, i motsetning til detail-siden under). +- `order-of-merit-detail.tsx` (innstillinger, eksisterende OOM): samme + `eclecticAllowed()`-mønster, men INGEN reset-logikk trengs -- `kind`/ + `result_type` er låst etter opprettelse her (vist eksplisitt i UI-en, + se ADR-074), så gatingen er ren visning av et fast faktum. Hjelpetekst + lagt til under Eclectic-valget som forklarer samme-bane-kravet. + +**Verifisert:** `python3 -m py_compile` + full +`./scripts/run_backend_tests.sh` (80/80 -- 3 nye tester i +`test_order_of_merit.py`: cross-tournament eclectic velger faktisk +beste-per-hull på TVERS av turneringer, ikke bare beste turnering eller +snitt (hånd-utregnet: par 1-9 fra turnering A + par 10-18 fra turnering +B = 72, `results_available == 2`), avvisning ved lenking av turnering på +annen bane, avvisning ved modus-bytte til eclectic med allerede ulike +baner lenket). `tsc --noEmit` rent + 45/45 vitest. + +Egen scratch-database + scratch `teecup_api`-container (port 18102, +live-montert kode, `TEECUP_DEV_LOG_MAGIC_LINKS=true` for å logge inn +uten SMTP) + lokal `next dev` (port 13102): seedet to turneringer på +samme bane (par hull 1-9/bogey 10-18 i A, omvendt i B) + én tredje +turnering på en ANNEN bane, opprettet en eclectic-OOM, lenket A+B -- +leaderboard viste 72 (bekreftet også direkte mot API før nettleser- +verifisering). Innstillinger-seksjonen viste "Eclectic" korrekt valgt +med hjelpeteksten synlig. Forsøk på å lenke turnering C (annen bane) ga +en tydelig, lesbar feilmelding i UI-et: "Eclectic-aggregering krever at +alle lenkede turneringer spilles på samme bane." Opprettelsesskjemaets +gating verifisert direkte i nettleseren: Eclectic-knappen dukker opp for +`Spiller`+`Stableford-sum`/`Bruttoscore-sum`/`Nettoscore-sum`, forsvinner +og aggregering nullstilles automatisk til `Sum` når resultattype endres +til `Poeng etter plassering` ELLER type endres til `Lag`. Lys+mørk +bekreftet (skjermbilde av Innstillinger-seksjonen i mørk modus). Scratch- +stacken fullstendig revet ned (database, rolle, container, MinIO-bøtte, +`next dev`) -- ekte `teecup_db`/`teecup_api`/`teecup_frontend` urørt. + +**Ingen migrasjon** -- ren kode-endring, `aggregation_mode='eclectic'` +fantes som gyldig verdi fra migrasjon 055. + +**Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build +teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra +bruker. Begge containere startet rent. + +--- + ## Utviklingsplan (rekkefølge) 1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1264c..2526b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11572,3 +11572,50 @@ Neste steg: **Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker. Begge containere startet rent. + +91. **Order of Merit: eclectic-aggregering på tvers av lenkede + turneringer (ADR-075) — 2026-08-15.** Tredje av de gjenstående OOM- + punktene fra ADR-043/074 -- sesong-"drømmerunde": beste resultat PER + HULL på tvers av ALLE lenkede turneringer (kun `result_type IN + ('stableford','gross','net')`), tidligere eksplisitt avvist i + leaderboard-endepunktet. + + Samme-bane-håndheving lagt til (ny, ingen migrasjon): en eclectic- + OOM kan ikke lenke en turnering på en annen bane enn de allerede + lenkede, verken ved lenking eller ved å bytte `aggregation_mode` til + eclectic med ulike baner allerede lenket. Datainnsamlingen (ny + `_compute_eclectic_oom_values`) gjenbruker `eclectic_best_per_hole()` + fra `handicap_engine.py` HELT UENDRET -- samme motor som ADR-068 + (flere runder i én turnering), kun datainnsamlingen foran er + tilpasset til å samle på tvers av turneringer i stedet, keyed på + `player_id` med en sammensatt (turnering-id, runde-sekvens)-kilde- + indeks. Frontend: "Eclectic" lagt til som tredje aggregeringsvalg + både i opprettelsesskjemaet (`order-of-merit-list.tsx`, med auto- + reset til Sum hvis kind/resultattype endres til noe eclectic ikke + støtter) og innstillinger for eksisterende OOM-er (`order-of-merit- + detail.tsx`, ingen reset nødvendig der siden kind/resultattype er + låst etter opprettelse) -- begge gated identisk til (og speilende) + serverens regel. + + **Verifisert:** full `./scripts/run_backend_tests.sh` (80/80 -- 3 nye + tester: cross-tournament eclectic velger faktisk beste-per-hull på + TVERS av turneringer med hånd-utregnet forventet verdi, avvisning + ved lenking på annen bane, avvisning ved modus-bytte med ulike baner + allerede lenket). `tsc --noEmit` rent + 45/45 vitest. Scratch- + database + scratch `teecup_api` (port 18102, live-montert kode, + `TEECUP_DEV_LOG_MAGIC_LINKS=true` for innlogging uten SMTP) + lokal + `next dev` (port 13102): opprettet ekte eclectic-OOM i nettleseren, + lenket to turneringer på samme bane -- leaderboard viste korrekt + kryss-turnering-tall (72). Forsøk på å lenke en turnering på en annen + bane ga en tydelig feilmelding i UI-et. Opprettelsesskjemaets gating + verifisert live (Eclectic dukker opp/forsvinner + nullstiller + korrekt ved endring av type/resultattype). Lys+mørk bekreftet. + Scratch-stacken fullstendig revet ned (database, rolle, container, + MinIO-bøtte, `next dev`) -- ekte `teecup_db`/`teecup_api`/ + `teecup_frontend` urørt. + + **Ingen migrasjon.** + + **Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build + teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra + bruker. Begge containere startet rent. diff --git a/app/routers/order_of_merit.py b/app/routers/order_of_merit.py index d0a8402..59a3eee 100644 --- a/app/routers/order_of_merit.py +++ b/app/routers/order_of_merit.py @@ -35,7 +35,14 @@ from pydantic import BaseModel, Field from ..auth import get_authorized_org from ..db import org_connection from ..errors import app_error, translate_db_errors -from handicap_engine import order_of_merit_aggregate, order_of_merit_points_for_position +from handicap_engine import ( + allocate_strokes_by_index, + EclecticHoleValue, + eclectic_best_per_hole, + order_of_merit_aggregate, + order_of_merit_points_for_position, + stableford_points_for_hole, +) from .individual_tournaments import _compute_individual_standings @@ -165,6 +172,24 @@ async def update_order_of_merit( raise app_error( 400, "VALIDATION_FAILED", "Eclectic krever resultattype Stableford, brutto eller netto." ) + if effective_mode == "eclectic": + # Slår PÅ eclectic mens flere turneringer med ulik bane + # allerede er lenket -- avvis her også, ikke bare ved lenking + # av en NY turnering (ADR-075). + links = await conn.fetch( + "SELECT tournament_id::text FROM order_of_merit_link WHERE order_of_merit_id = $1", oom_id + ) + course_ids = set() + for link in links: + cid = await _course_id_for_tournament(conn, link["tournament_id"]) + if cid is not None: + course_ids.add(cid) + if len(course_ids) > 1: + raise app_error( + 400, + "VALIDATION_FAILED", + "Eclectic-aggregering krever at alle lenkede turneringer spilles på samme bane.", + ) row = await conn.fetchrow( f""" UPDATE order_of_merit SET {', '.join(set_clauses)}, updated_at = now() @@ -248,6 +273,39 @@ def _parse_link_row(r) -> dict: return d +async def _course_id_for_tournament(conn, tournament_id: str) -> str | None: + """Samme mønster som _attach_eclectic_totals sin egen bane-oppslag + (individual_tournaments.py) -- LIMIT 1 antar (bevisst, håndhevet av + resten av appen ved rundeopprettelse) at en turnerings runder deler + samme bane.""" + return await conn.fetchval( + "SELECT course_id::text FROM tournament_round WHERE tournament_id = $1 LIMIT 1", tournament_id + ) + + +async def _validate_same_course_for_eclectic(conn, oom_id: str, new_tournament_id: str) -> None: + """Eclectic-aggregering (ADR-075) plukker beste resultat PER HULL på + tvers av alle lenkede turneringer -- meningsløst hvis banene ikke er + de samme (hull 7 på én bane er ikke hull 7 på en annen). Håndhevet + her (lenke-tidspunkt) OG i update_order_of_merit (modus-endring), + samme "fail loudly, ikke stille feil tall"-filosofi som ADR-068/ + ADR-019 Beslutning C.""" + new_course_id = await _course_id_for_tournament(conn, new_tournament_id) + if new_course_id is None: + return # ingen runder ennå på den nye turneringen -- ingenting å sammenligne mot + links = await conn.fetch( + "SELECT tournament_id::text FROM order_of_merit_link WHERE order_of_merit_id = $1", oom_id + ) + for link in links: + existing_course_id = await _course_id_for_tournament(conn, link["tournament_id"]) + if existing_course_id is not None and existing_course_id != new_course_id: + raise app_error( + 400, + "VALIDATION_FAILED", + "Eclectic-aggregering krever at alle lenkede turneringer spilles på samme bane.", + ) + + @router.post( "/orgs/{organization_id}/order-of-merits/{oom_id}/tournaments", response_model=OrderOfMeritLinkOut, @@ -259,7 +317,7 @@ async def link_tournament_to_order_of_merit( organization_id: str = Depends(get_authorized_org), ) -> OrderOfMeritLinkOut: async with org_connection(organization_id) as conn, translate_db_errors(): - oom = await conn.fetchrow("SELECT result_type FROM order_of_merit WHERE id = $1", oom_id) + oom = await conn.fetchrow("SELECT result_type, aggregation_mode FROM order_of_merit WHERE id = $1", oom_id) if oom is None: raise app_error(404, "NOT_FOUND", "Order of Merit finnes ikke.") tournament = await conn.fetchrow( @@ -271,6 +329,8 @@ async def link_tournament_to_order_of_merit( raise app_error( 400, "VALIDATION_FAILED", "Kun individuelle turneringer kan lenkes til en Order of Merit." ) + if oom["aggregation_mode"] == "eclectic": + await _validate_same_course_for_eclectic(conn, oom_id, body.tournament_id) result_type = oom["result_type"] if result_type == "points" and not body.points_table: @@ -496,6 +556,117 @@ async def remove_order_of_merit_team_member( raise app_error(404, "NOT_FOUND", "Medlemmet finnes ikke.") +# --------------------------------------------------------------------------- +# Eclectic-aggregering på tvers av lenkede turneringer (ADR-075) -- sesong- +# "drømmerunde": beste resultat PER HULL på tvers av ALLE en spillers +# lenkede turneringer (bekreftet samme bane av _validate_same_course_for_ +# eclectic ved lenking/modus-endring). IKKE det samme som ADR-068 sitt +# Eclectic-format (scoping til RUNDENE innenfor ÉN turnering) -- samme +# motorfunksjon (`eclectic_best_per_hole`) gjenbrukes UENDRET, men +# datainnsamlingen her spenner over FLERE turneringers `tournament_round_ +# hole`, ikke bare én. Nøkkelen er `player_id` (samme spiller har ulik +# `tournament_participant_id` per turnering), ikke `tournament_ +# participant_id` slik innad-i-én-turnering-varianten (`_attach_eclectic_ +# totals` i individual_tournaments.py) bruker. +# --------------------------------------------------------------------------- + +async def _compute_eclectic_oom_values( + conn, tournament_ids: list[str], result_type: Literal["gross", "net", "stableford"] +) -> tuple[dict[str, float], dict[str, str], dict[str, int]]: + """Returnerer (totaler, navn, antall-kilde-turneringer) per spiller. + Eclectic-aggregering ER selve tvers-av-turneringer-sammenslåingen -- + det finnes ingen ekstra "sum/snitt av N eclectic-totaler" ovenpå + (`order_of_merit_aggregate` er derfor bevisst IKKE brukt her, se + kalleren).""" + if not tournament_ids: + return {}, {}, {} + + course_id = None + for tid in tournament_ids: + course_id = await _course_id_for_tournament(conn, tid) + if course_id is not None: + break + if course_id is None: + return {}, {}, {} + + hole_rows = await conn.fetch( + "SELECT hole_number, par, stroke_index FROM hole WHERE course_id = $1 ORDER BY hole_number", course_id + ) + par_by_hole = {r["hole_number"]: r["par"] for r in hole_rows} + si_by_hole = [r["stroke_index"] for r in hole_rows] + + rows = await conn.fetch( + """ + SELECT tr.tournament_id::text AS tournament_id, tr.sequence, + tp.player_id::text AS player_id, p.display_name AS player_name, + trp.course_handicap, trh.hole_number, trh.gross_strokes + FROM tournament_round_hole trh + JOIN tournament_round_participant trp ON trp.id = trh.tournament_round_participant_id + JOIN tournament_round tr ON tr.id = trp.tournament_round_id + JOIN tournament_participant tp ON tp.id = trp.tournament_participant_id + JOIN player p ON p.id = tp.player_id + WHERE tr.tournament_id = ANY($1::uuid[]) AND trh.gross_strokes IS NOT NULL + ORDER BY tr.tournament_id, tr.sequence + """, + tournament_ids, + ) + + # Sammensatt kilde-indeks: (turnering-id, sekvens) -> global indeks, + # siden `sequence` kun er unik INNENFOR én turnering (speiler + # _attach_eclectic_totals sin sequence_by_index, men på tvers av flere + # turneringer i stedet for kun én). + index_by_source: dict[tuple[str, int], int] = {} + source_by_index: dict[int, tuple[str, int]] = {} + for r in rows: + key = (r["tournament_id"], r["sequence"]) + if key not in index_by_source: + idx = len(index_by_source) + index_by_source[key] = idx + source_by_index[idx] = key + + allocation_cache: dict[int, list[int]] = {} + values_by_player: dict[str, list[list[EclecticHoleValue]]] = {} + names_by_player: dict[str, str] = {} + + for r in rows: + if r["course_handicap"] is None: + continue + pid = r["player_id"] + names_by_player[pid] = r["player_name"] + if pid not in values_by_player: + values_by_player[pid] = [[] for _ in range(18)] + + strokes_received = 0 + if result_type != "gross": + hcp = r["course_handicap"] + if hcp not in allocation_cache: + allocation_cache[hcp] = allocate_strokes_by_index(hcp, si_by_hole) + strokes_received = allocation_cache[hcp][r["hole_number"] - 1] + + if result_type == "gross": + value = r["gross_strokes"] + elif result_type == "net": + value = r["gross_strokes"] - strokes_received + else: + value = stableford_points_for_hole(par_by_hole[r["hole_number"]], r["gross_strokes"], strokes_received) + + source_index = index_by_source[(r["tournament_id"], r["sequence"])] + values_by_player[pid][r["hole_number"] - 1].append( + EclecticHoleValue(round_index=source_index, value=value) + ) + + totals_by_player: dict[str, float] = {} + tournament_count_by_player: dict[str, int] = {} + for pid, values_by_hole in values_by_player.items(): + result = eclectic_best_per_hole(values_by_hole, result_type) + if not result.holes: + continue + totals_by_player[pid] = float(result.total) + tournament_count_by_player[pid] = len({source_by_index[h.round_index][0] for h in result.holes}) + + return totals_by_player, names_by_player, tournament_count_by_player + + # --------------------------------------------------------------------------- # Leaderboard -- "regn ut ved lesing", ingen cache-tabell (samme filosofi # som Nassau/High-low-high/individual_leaderboard selv). @@ -524,10 +695,14 @@ def _money_for_position(position_label: str, payout_table: list[dict]) -> float: return 0.0 -def _player_eligibility(raw_values: list[float], oom, birth_year: int | None) -> tuple[bool, str | None]: +def _player_eligibility(results_available: int, oom, birth_year: int | None) -> tuple[bool, str | None]: """Delt mellom spiller- og lag-leaderboardet (et lags medlem er - eligibility-sjekket EKSAKT som en spiller ville vært alene).""" - results_available = len(raw_values) + eligibility-sjekket EKSAKT som en spiller ville vært alene), OG mellom + sum/snitt (`results_available = len(raw_values)`) og eclectic + (`results_available` = antall KILDE-turneringer som faktisk bidro et + hull til sesong-drømmerunden, se _compute_eclectic_oom_values) -- + tallet betyr det samme ("hvor mange turneringer talte") uansett hvilken + modus som produserte det.""" if oom["birth_year_from"] is not None or oom["birth_year_to"] is not None: if birth_year is None: return False, "Ingen registrert fødselsdato." @@ -571,11 +746,10 @@ async def order_of_merit_leaderboard( ) if oom is None: raise app_error(404, "NOT_FOUND", "Order of Merit finnes ikke.") - if oom["aggregation_mode"] == "eclectic": - raise app_error(400, "VALIDATION_FAILED", "Eclectic-aggregering er ikke bygget ennå.") result_type = oom["result_type"] lower_is_better = result_type in ("gross", "net") + is_eclectic = oom["aggregation_mode"] == "eclectic" links = await conn.fetch( """ @@ -587,21 +761,40 @@ async def order_of_merit_leaderboard( ) # player_id -> [rå bidrags-verdier, ett per lenket turnering spilleren har deltatt i] + # (eclectic-modus: nøyaktig ÉN verdi per spiller -- sesongens + # ferdig-sammenslåtte drømmerunde-total, se _compute_eclectic_oom_ + # values. "results_available" betyr da antall KILDE-turneringer + # som bidro et hull, ikke antall verdier i denne listen.) values_by_player: dict[str, list[float]] = {} names_by_player: dict[str, str] = {} + results_available_by_player: dict[str, int] = {} - for link in links: - tournament_row, entries = await _compute_individual_standings(conn, link["tournament_id"]) - points_table = _decode_json(link["points_table"]) - money_payout_table = _decode_json(link["money_payout_table"]) - for e in entries: - contribution = _contribution_for_entry( - e, result_type, points_table, money_payout_table, tournament_row["scoring_method"] - ) - if contribution is None: - continue - values_by_player.setdefault(e.player_id, []).append(contribution) - names_by_player[e.player_id] = e.player_name + if is_eclectic: + # ADR-075: krever result_type IN (stableford,gross,net) og + # kind='player', begge håndhevet ved opprettelse/PATCH -- + # ingen ekstra sjekk nødvendig her. + tournament_ids = [link["tournament_id"] for link in links] + totals, names_by_player, tournament_counts = await _compute_eclectic_oom_values( + conn, tournament_ids, result_type + ) + for player_id, total in totals.items(): + values_by_player[player_id] = [total] + results_available_by_player[player_id] = tournament_counts[player_id] + else: + for link in links: + tournament_row, entries = await _compute_individual_standings(conn, link["tournament_id"]) + points_table = _decode_json(link["points_table"]) + money_payout_table = _decode_json(link["money_payout_table"]) + for e in entries: + contribution = _contribution_for_entry( + e, result_type, points_table, money_payout_table, tournament_row["scoring_method"] + ) + if contribution is None: + continue + values_by_player.setdefault(e.player_id, []).append(contribution) + names_by_player[e.player_id] = e.player_name + for player_id, raw_values in values_by_player.items(): + results_available_by_player[player_id] = len(raw_values) # Aldersgrense: filtrert FØR aggregering (spillere utenfor # fødselsårs-intervallet er ikke rangert i det hele tatt, ikke @@ -620,10 +813,21 @@ async def order_of_merit_leaderboard( if oom["kind"] == "player": for player_id, raw_values in values_by_player.items(): - eligible, ineligible_reason = _player_eligibility(raw_values, oom, birth_years.get(player_id)) - value = _aggregate_value(raw_values, oom, lower_is_better) - results_available = len(raw_values) - counted = min(oom["count_best_n"], results_available) if oom["count_best_n"] is not None else results_available + results_available = results_available_by_player[player_id] + eligible, ineligible_reason = _player_eligibility(results_available, oom, birth_years.get(player_id)) + # Eclectic: raw_values er allerede ÉN ferdig-sammenslått + # sesongtotal (se _compute_eclectic_oom_values) -- ingen + # order_of_merit_aggregate-kall (som uansett kun kjenner + # sum/snitt, ikke eclectic, se motoren), og count_best_n + # gir ingen mening ("beste N av ÉN verdi"). + if is_eclectic: + value = raw_values[0] + counted = results_available + else: + value = _aggregate_value(raw_values, oom, lower_is_better) + counted = ( + min(oom["count_best_n"], results_available) if oom["count_best_n"] is not None else results_available + ) results.append( OrderOfMeritLeaderboardEntry( @@ -667,7 +871,9 @@ async def order_of_merit_leaderboard( raw_values = values_by_player.get(pid) if not raw_values: continue - member_eligible, _ = _player_eligibility(raw_values, oom, birth_years.get(pid)) + member_eligible, _ = _player_eligibility( + results_available_by_player[pid], oom, birth_years.get(pid) + ) if not member_eligible: continue member_value = _aggregate_value(raw_values, oom, lower_is_better) diff --git a/frontend/components/order-of-merit-detail.tsx b/frontend/components/order-of-merit-detail.tsx index 6073009..f3eaa85 100644 --- a/frontend/components/order-of-merit-detail.tsx +++ b/frontend/components/order-of-merit-detail.tsx @@ -18,7 +18,16 @@ import { cn } from "@/lib/utils" type OomKind = "player" | "team" type OomResultType = "points" | "stableford" | "gross" | "net" | "money" -type OomAggregationMode = "sum" | "average" +type OomAggregationMode = "sum" | "average" | "eclectic" + +// Eclectic-aggregering (ADR-075) krever kind='player' og en resultattype med +// per-hull-granularitet (stableford/brutto/netto) -- håndhevet server-side +// (order_of_merit.py). Kind/resultattype er låst etter opprettelse her, så +// ingen reset-logikk trengs (i motsetning til order-of-merit-list.tsx sitt +// opprettelsesskjema). +function eclecticAllowed(kind: OomKind, resultType: OomResultType): boolean { + return kind === "player" && (resultType === "stableford" || resultType === "gross" || resultType === "net") +} type ApiOrderOfMerit = { id: string @@ -656,14 +665,25 @@ function SettingsSection({ oom, onSave }: { oom: ApiOrderOfMerit; onSave: (patch
+ Eclectic krever at alle lenkede turneringer spilles på samme bane -- lenking av en turnering på en annen + bane avvises så lenge denne modusen er valgt. +
+ )}