Sesong-"drømmerunde" -- beste resultat per hull på tvers av ALLE lenkede turneringer (stableford/brutto/netto), med samme-bane-håndheving ved lenking og modus-bytte. Gjenbruker eclectic_best_per_hole() uendret fra handicap_engine.py. Frontend: "Eclectic" lagt til i både opprettelses- skjema og innstillinger, gated til gyldige kind/resultattype-kombinasjoner. 80/80 backend-tester, 45/45 vitest, scratch-verifisert i nettleser (lys+mørk). Rullet ut. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
390 lines
18 KiB
Python
390 lines
18 KiB
Python
"""
|
|
Order of Merit (ADR-043) -- Steg 0/1/2-tester (ADR-074/075). Kaller de
|
|
faktiske router-funksjonene direkte (samme mønster som
|
|
test_tournament_hole_stats.py/test_hole_history.py).
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from app.auth import CurrentUser
|
|
from app.routers.individual_tournaments import (
|
|
HoleUpdate as TournamentHoleUpdate,
|
|
update_hole as tournament_update_hole,
|
|
)
|
|
from app.routers.order_of_merit import (
|
|
OrderOfMeritCreate,
|
|
OrderOfMeritLinkCreate,
|
|
OrderOfMeritTeamCreate,
|
|
OrderOfMeritTeamMemberCreate,
|
|
OrderOfMeritUpdate,
|
|
add_order_of_merit_team_member,
|
|
create_order_of_merit,
|
|
create_order_of_merit_team,
|
|
delete_order_of_merit_team,
|
|
link_tournament_to_order_of_merit,
|
|
list_order_of_merit_teams,
|
|
order_of_merit_leaderboard,
|
|
remove_order_of_merit_team_member,
|
|
update_order_of_merit,
|
|
)
|
|
|
|
from tests.conftest import (
|
|
add_membership,
|
|
create_org,
|
|
create_org_hole,
|
|
create_course,
|
|
create_player,
|
|
create_tee,
|
|
create_tournament,
|
|
create_tournament_participant,
|
|
create_tournament_round,
|
|
create_tournament_round_participant,
|
|
create_user,
|
|
)
|
|
|
|
import app.db as app_db
|
|
|
|
|
|
async def _score_all_holes(tournament_id, round_id, rp_id, org_id, user_id, score):
|
|
user = CurrentUser(user_id=user_id)
|
|
for n in range(1, 19):
|
|
await tournament_update_hole(
|
|
tournament_id, round_id, rp_id, n,
|
|
TournamentHoleUpdate(gross_strokes=score, expected_version=None),
|
|
organization_id=org_id, user=user,
|
|
)
|
|
|
|
|
|
async def _setup_eclectic_tournament(org_id, user_id, method="eclectic_stableford"):
|
|
tournament_id = await create_tournament(org_id, name="Eclectic OOM-turnering")
|
|
course_id = await create_course(org_id, name="OOM Links")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
|
|
tee_id = await create_tee(org_id, course_id)
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute(
|
|
"UPDATE tournament SET format_type = 'individual', scoring_method = $2 WHERE id = $1",
|
|
tournament_id, method,
|
|
)
|
|
player_id = await create_player(org_id, user_id=user_id, display_name="OOM Spiller")
|
|
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
|
|
|
|
round1_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
|
round2_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=2)
|
|
rp1_id = await create_tournament_round_participant(org_id, round1_id, tp_id, tee_id, course_handicap=0)
|
|
rp2_id = await create_tournament_round_participant(org_id, round2_id, tp_id, tee_id, course_handicap=0)
|
|
|
|
# Runde 1: par (4) på alle 18 hull -> 2 poeng/hull = 36 Stableford-poeng.
|
|
await _score_all_holes(tournament_id, round1_id, rp1_id, org_id, user_id, score=4)
|
|
# Runde 2: bogey (5) på alle 18 hull -> 1 poeng/hull = 18 Stableford-poeng.
|
|
await _score_all_holes(tournament_id, round2_id, rp2_id, org_id, user_id, score=5)
|
|
# Rå SUM over begge runder = 36 + 18 = 54. Eclectic (beste PER HULL,
|
|
# runde 1 vinner hver gang) = 18 hull * 2 poeng = 36 -- tydelig
|
|
# forskjellig fra rå-summen, beviser at riktig tall faktisk brukes.
|
|
return tournament_id, player_id
|
|
|
|
|
|
async def test_oom_stableford_uses_eclectic_total_not_raw_sum(pool):
|
|
"""ADR-074: en lenket eclectic_stableford-turnering skal bidra med
|
|
entry.eclectic_total (36), IKKE rå stableford_total på tvers av
|
|
begge runder (54)."""
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
tournament_id, player_id = await _setup_eclectic_tournament(org_id, owner_id)
|
|
|
|
oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Sesong-OOM", kind="player", result_type="stableford", aggregation_mode="sum"),
|
|
organization_id=org_id,
|
|
)
|
|
await link_tournament_to_order_of_merit(
|
|
oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_id), organization_id=org_id
|
|
)
|
|
|
|
leaderboard = await order_of_merit_leaderboard(oom.id, organization_id=org_id)
|
|
assert len(leaderboard) == 1
|
|
assert leaderboard[0].value == 36.0
|
|
assert leaderboard[0].value != 54.0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Steg 1 (ADR-074): lag-OOM CRUD + leaderboard.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _setup_stroke_gross_tournament(org_id, name, participants: list[tuple[str, int]]):
|
|
"""participants: liste av (player_id, gross-score-per-hull) -- ETT
|
|
18-hulls slagspill-runde, samme score på alle 18 hull for enkelhets
|
|
skyld."""
|
|
tournament_id = await create_tournament(org_id, name=name)
|
|
course_id = await create_course(org_id, name=f"{name} bane")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
|
|
tee_id = await create_tee(org_id, course_id)
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute(
|
|
"UPDATE tournament SET format_type = 'individual', scoring_method = 'stroke_gross' WHERE id = $1",
|
|
tournament_id,
|
|
)
|
|
round_id = await create_tournament_round(org_id, tournament_id, course_id)
|
|
for player_id, score in participants:
|
|
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
|
|
rp_id = await create_tournament_round_participant(org_id, round_id, tp_id, tee_id, course_handicap=0)
|
|
user_id = None
|
|
async with app_db.org_connection(org_id) as conn:
|
|
user_id = await conn.fetchval("SELECT user_id::text FROM player WHERE id = $1", player_id)
|
|
await _score_all_holes(tournament_id, round_id, rp_id, org_id, user_id, score=score)
|
|
return tournament_id
|
|
|
|
|
|
async def test_team_leaderboard_sums_member_individual_totals(pool):
|
|
"""To turneringer lenket til en gross/sum lag-OOM. Spiller 1: 72+90=162.
|
|
Spiller 2: 90+108=198. Ett lag med begge -> forventet lagsum 360."""
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
user1_id = await create_user(display_name="Spiller Én")
|
|
user2_id = await create_user(display_name="Spiller To")
|
|
player1_id = await create_player(org_id, user_id=user1_id, display_name="Spiller Én")
|
|
player2_id = await create_player(org_id, user_id=user2_id, display_name="Spiller To")
|
|
|
|
tournament_a = await _setup_stroke_gross_tournament(
|
|
org_id, "Turnering A", [(player1_id, 4), (player2_id, 5)]
|
|
)
|
|
tournament_b = await _setup_stroke_gross_tournament(
|
|
org_id, "Turnering B", [(player1_id, 5), (player2_id, 6)]
|
|
)
|
|
|
|
oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Lag-OOM", kind="team", result_type="gross", aggregation_mode="sum"),
|
|
organization_id=org_id,
|
|
)
|
|
await link_tournament_to_order_of_merit(
|
|
oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_a), organization_id=org_id
|
|
)
|
|
await link_tournament_to_order_of_merit(
|
|
oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_b), organization_id=org_id
|
|
)
|
|
|
|
team = await create_order_of_merit_team(
|
|
oom.id, OrderOfMeritTeamCreate(name="Laget"), organization_id=org_id
|
|
)
|
|
await add_order_of_merit_team_member(
|
|
oom.id, team.id, OrderOfMeritTeamMemberCreate(player_id=player1_id), organization_id=org_id
|
|
)
|
|
await add_order_of_merit_team_member(
|
|
oom.id, team.id, OrderOfMeritTeamMemberCreate(player_id=player2_id), organization_id=org_id
|
|
)
|
|
|
|
leaderboard = await order_of_merit_leaderboard(oom.id, organization_id=org_id)
|
|
assert len(leaderboard) == 1
|
|
assert leaderboard[0].player_id == team.id
|
|
assert leaderboard[0].player_name == "Laget"
|
|
assert leaderboard[0].value == 360.0
|
|
assert leaderboard[0].results_available == 2
|
|
assert leaderboard[0].results_counted == 2
|
|
assert leaderboard[0].eligible is True
|
|
|
|
|
|
async def test_team_leaderboard_best_n_applies_at_both_levels(pool):
|
|
"""Samme oppsett som over, men count_best_n=1. ADR-074: ETT lagret
|
|
aggregeringsvalg styrer BEGGE nivåer -- count_best_n=1 betyr derfor
|
|
"hver spillers beste 1 av 2 turneringer" OG "lagets beste 1 av 2
|
|
medlemmer", ikke bare det ene. Spiller 1 sin beste (laveste) turnering
|
|
er 72, spiller 2 sin er 90 -- laget teller da KUN 72 (sitt beste
|
|
medlem, som allerede er redusert til sitt eget beste resultat)."""
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
user1_id = await create_user()
|
|
user2_id = await create_user()
|
|
player1_id = await create_player(org_id, user_id=user1_id)
|
|
player2_id = await create_player(org_id, user_id=user2_id)
|
|
|
|
tournament_a = await _setup_stroke_gross_tournament(org_id, "A", [(player1_id, 4), (player2_id, 5)])
|
|
tournament_b = await _setup_stroke_gross_tournament(org_id, "B", [(player1_id, 5), (player2_id, 6)])
|
|
|
|
oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(
|
|
name="Lag-OOM beste-1", kind="team", result_type="gross", aggregation_mode="sum", count_best_n=1
|
|
),
|
|
organization_id=org_id,
|
|
)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_a), organization_id=org_id)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_b), organization_id=org_id)
|
|
|
|
team = await create_order_of_merit_team(oom.id, OrderOfMeritTeamCreate(name="Laget"), organization_id=org_id)
|
|
await add_order_of_merit_team_member(oom.id, team.id, OrderOfMeritTeamMemberCreate(player_id=player1_id), organization_id=org_id)
|
|
await add_order_of_merit_team_member(oom.id, team.id, OrderOfMeritTeamMemberCreate(player_id=player2_id), organization_id=org_id)
|
|
|
|
leaderboard = await order_of_merit_leaderboard(oom.id, organization_id=org_id)
|
|
assert leaderboard[0].value == 72.0
|
|
assert leaderboard[0].results_counted == 1
|
|
|
|
|
|
async def test_team_crud_and_kind_guard(pool):
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
player_id = await create_player(org_id)
|
|
|
|
player_oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Spiller-OOM", kind="player", result_type="gross"), organization_id=org_id
|
|
)
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await create_order_of_merit_team(
|
|
player_oom.id, OrderOfMeritTeamCreate(name="Skal feile"), organization_id=org_id
|
|
)
|
|
assert exc_info.value.status_code == 400
|
|
|
|
team_oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Lag-OOM", kind="team", result_type="gross"), organization_id=org_id
|
|
)
|
|
team = await create_order_of_merit_team(team_oom.id, OrderOfMeritTeamCreate(name="Laget"), organization_id=org_id)
|
|
member = await add_order_of_merit_team_member(
|
|
team_oom.id, team.id, OrderOfMeritTeamMemberCreate(player_id=player_id), organization_id=org_id
|
|
)
|
|
|
|
teams = await list_order_of_merit_teams(team_oom.id, organization_id=org_id)
|
|
assert len(teams) == 1
|
|
assert len(teams[0].members) == 1
|
|
assert teams[0].members[0].player_id == player_id
|
|
|
|
await remove_order_of_merit_team_member(team_oom.id, team.id, member.id, organization_id=org_id)
|
|
teams = await list_order_of_merit_teams(team_oom.id, organization_id=org_id)
|
|
assert len(teams[0].members) == 0
|
|
|
|
await delete_order_of_merit_team(team_oom.id, team.id, organization_id=org_id)
|
|
teams = await list_order_of_merit_teams(team_oom.id, organization_id=org_id)
|
|
assert len(teams) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Steg 2 (ADR-075): eclectic-aggregering på tvers av lenkede turneringer.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _setup_tournament_on_course(org_id, name, course_id, tee_id, participants: list[tuple[str, str, list[int]]]):
|
|
"""participants: liste av (player_id, user_id, [score per hull 1..18])
|
|
-- DELER en allerede opprettet bane med kalleren (kreves for eclectic-
|
|
testene, der to turneringer må ha SAMME course_id)."""
|
|
tournament_id = await create_tournament(org_id, name=name)
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute(
|
|
"UPDATE tournament SET format_type = 'individual', scoring_method = 'stroke_gross' WHERE id = $1",
|
|
tournament_id,
|
|
)
|
|
round_id = await create_tournament_round(org_id, tournament_id, course_id)
|
|
for player_id, user_id, scores in participants:
|
|
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
|
|
rp_id = await create_tournament_round_participant(org_id, round_id, tp_id, tee_id, course_handicap=0)
|
|
user = CurrentUser(user_id=user_id)
|
|
for n, score in enumerate(scores, start=1):
|
|
await tournament_update_hole(
|
|
tournament_id, round_id, rp_id, n,
|
|
TournamentHoleUpdate(gross_strokes=score, expected_version=None),
|
|
organization_id=org_id, user=user,
|
|
)
|
|
return tournament_id
|
|
|
|
|
|
async def test_eclectic_oom_picks_best_hole_across_tournaments(pool):
|
|
"""To turneringer, SAMME bane. Spiller: par (4) på hull 1-9 i A, bogey
|
|
(5) på hull 10-18 i A -- omvendt i B. Eclectic gross skal plukke par
|
|
(4) på ALLE 18 hull (9 fra A, 9 fra B) = 72, IKKE 81 (snitt) eller noe
|
|
annet -- beviser faktisk kryss-turnering-utvelgelse, samme bevis-
|
|
mønster som ADR-068 sin egen innad-i-turnering-test."""
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
user_id = await create_user()
|
|
player_id = await create_player(org_id, user_id=user_id)
|
|
|
|
course_id = await create_course(org_id, name="Delt bane")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
|
|
tee_id = await create_tee(org_id, course_id)
|
|
|
|
scores_a = [4] * 9 + [5] * 9 # par hull 1-9, bogey hull 10-18
|
|
scores_b = [5] * 9 + [4] * 9 # bogey hull 1-9, par hull 10-18
|
|
tournament_a = await _setup_tournament_on_course(org_id, "A", course_id, tee_id, [(player_id, user_id, scores_a)])
|
|
tournament_b = await _setup_tournament_on_course(org_id, "B", course_id, tee_id, [(player_id, user_id, scores_b)])
|
|
|
|
oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Eclectic-OOM", kind="player", result_type="gross", aggregation_mode="eclectic"),
|
|
organization_id=org_id,
|
|
)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_a), organization_id=org_id)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_b), organization_id=org_id)
|
|
|
|
leaderboard = await order_of_merit_leaderboard(oom.id, organization_id=org_id)
|
|
assert len(leaderboard) == 1
|
|
assert leaderboard[0].value == 72.0
|
|
assert leaderboard[0].results_available == 2 # begge turneringene bidro minst ett hull
|
|
|
|
|
|
async def test_eclectic_oom_rejects_linking_tournament_on_different_course(pool):
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
user_id = await create_user()
|
|
player_id = await create_player(org_id, user_id=user_id)
|
|
|
|
course_a = await create_course(org_id, name="Bane A")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_a, hole_number=n, par=4, stroke_index=n)
|
|
tee_a = await create_tee(org_id, course_a)
|
|
tournament_a = await _setup_tournament_on_course(org_id, "A", course_a, tee_a, [(player_id, user_id, [4] * 18)])
|
|
|
|
course_b = await create_course(org_id, name="Bane B")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_b, hole_number=n, par=4, stroke_index=n)
|
|
tee_b = await create_tee(org_id, course_b)
|
|
tournament_b = await _setup_tournament_on_course(org_id, "B", course_b, tee_b, [(player_id, user_id, [5] * 18)])
|
|
|
|
oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Eclectic-OOM", kind="player", result_type="gross", aggregation_mode="eclectic"),
|
|
organization_id=org_id,
|
|
)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_a), organization_id=org_id)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await link_tournament_to_order_of_merit(
|
|
oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_b), organization_id=org_id
|
|
)
|
|
assert exc_info.value.status_code == 400
|
|
|
|
|
|
async def test_eclectic_oom_rejects_mode_switch_with_existing_different_course_links(pool):
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
user_id = await create_user()
|
|
player_id = await create_player(org_id, user_id=user_id)
|
|
|
|
course_a = await create_course(org_id, name="Bane A")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_a, hole_number=n, par=4, stroke_index=n)
|
|
tee_a = await create_tee(org_id, course_a)
|
|
tournament_a = await _setup_tournament_on_course(org_id, "A", course_a, tee_a, [(player_id, user_id, [4] * 18)])
|
|
|
|
course_b = await create_course(org_id, name="Bane B")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_b, hole_number=n, par=4, stroke_index=n)
|
|
tee_b = await create_tee(org_id, course_b)
|
|
tournament_b = await _setup_tournament_on_course(org_id, "B", course_b, tee_b, [(player_id, user_id, [5] * 18)])
|
|
|
|
# Opprettes med sum (ikke eclectic), slik at begge ulikbane-turneringene
|
|
# kan lenkes uten å treffe lenke-tidspunktets sjekk -- selve bug-en som
|
|
# skal testes er modus-BYTTET etterpå.
|
|
oom = await create_order_of_merit(
|
|
OrderOfMeritCreate(name="Sum-OOM", kind="player", result_type="gross", aggregation_mode="sum"),
|
|
organization_id=org_id,
|
|
)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_a), organization_id=org_id)
|
|
await link_tournament_to_order_of_merit(oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_b), organization_id=org_id)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await update_order_of_merit(
|
|
oom.id, OrderOfMeritUpdate(aggregation_mode="eclectic"), organization_id=org_id
|
|
)
|
|
assert exc_info.value.status_code == 400
|