""" 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, get_public_order_of_merit, 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, new_id, ) 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 # --------------------------------------------------------------------------- # Steg 3 (ADR-076): offentlig/delt visning -- migrasjon 076 # (public_order_of_merit_by_id) + /public/order-of-merits/{id}. # --------------------------------------------------------------------------- async def test_public_order_of_merit_returns_404_for_nonexistent_id(pool): with pytest.raises(HTTPException) as exc_info: await get_public_order_of_merit(new_id()) assert exc_info.value.status_code == 404 async def test_public_order_of_merit_returns_404_for_private_oom(pool): """public_visible=false (default) -- samme anti-enumerering som public_org_by_slug/public_tournament_by_code: en privat OOM skal gi NØYAKTIG samme 404 som en ikke-eksisterende id, ikke en 403 som ville røpet at id-en faktisk finnes.""" org_id = await create_org() owner_id = await create_user() await add_membership(org_id, owner_id, role="owner") oom = await create_order_of_merit( OrderOfMeritCreate(name="Privat OOM", kind="player", result_type="gross", public_visible=False), organization_id=org_id, ) with pytest.raises(HTTPException) as exc_info: await get_public_order_of_merit(oom.id) assert exc_info.value.status_code == 404 async def test_public_order_of_merit_returns_leaderboard_when_visible(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, display_name="Offentlig Spiller") tournament_id = await _setup_stroke_gross_tournament(org_id, "Offentlig turnering", [(player_id, 4)]) oom = await create_order_of_merit( OrderOfMeritCreate(name="Offentlig OOM", kind="player", result_type="gross", public_visible=True), organization_id=org_id, ) await link_tournament_to_order_of_merit( oom.id, OrderOfMeritLinkCreate(tournament_id=tournament_id), organization_id=org_id ) result = await get_public_order_of_merit(oom.id) assert result.name == "Offentlig OOM" assert result.kind == "player" assert result.result_type == "gross" assert len(result.entries) == 1 assert result.entries[0].player_name == "Offentlig Spiller" assert result.entries[0].value == 72.0