diff --git a/088_shotgun_start.sql b/088_shotgun_start.sql new file mode 100644 index 0000000..66af770 --- /dev/null +++ b/088_shotgun_start.sql @@ -0,0 +1,48 @@ +-- ===================================================================== +-- TeeCup — migrasjon 088 +-- Shotgun-start: alternativ til den tradisjonelle, staggerte starten +-- ===================================================================== +-- Reverserer/utvider EKSPLISITT migrasjon 084 sitt bevisste "IKKE +-- shotgun"-valg fra 2026-08-18 -- se ADR-096 for full begrunnelse for +-- hvorfor dette nå bygges. Samme arkitektoniske hull fantes BEGGE +-- steder rundeplanlegging finnes i appen: individuelle turneringer +-- (tournament_round/tournament_round_group) og lag/Cup-format +-- (session/match) -- ett delt starthull for HELE runden/økten, kun +-- ulikt KLOKKESLETT per gruppe/match, aldri ulikt HULL. Rettet +-- identisk begge steder. +-- +-- start_mode = 'consecutive' (standard, uendret oppførsel -- INGEN +-- regresjon): alle grupper/matcher deler samme starthull +-- (tournament_round.start_hole / session.start_hole, uendret), staggert +-- KUN på klokkeslett (eksisterende tee_time_override-mønster, uendret). +-- +-- start_mode = 'shotgun': ALLE grupper/matcher i runden/økten går ut +-- SAMTIDIG (intervallet blir meningsløst og ignoreres av app-laget i +-- denne modusen) -- fra HVER SIN starthull i stedet. Nytt, valgfritt +-- per-gruppe/per-match starthull-felt dekker dette -- NULL når runden/ +-- økten uansett er 'consecutive' (bruker da forelderens delte +-- starthull som før), settes eksplisitt av organisator per gruppe/ +-- match kun i shotgun-modus. +-- +-- Samme text+CHECK-mønster som migrasjon 087 (ikke en ny +-- CREATE TYPE ... AS ENUM, konsistent med nyere migrasjoner i denne +-- filserien). +-- ===================================================================== + +\set ON_ERROR_STOP on + +ALTER TABLE tournament_round + ADD COLUMN start_mode text NOT NULL DEFAULT 'consecutive' + CHECK (start_mode IN ('consecutive', 'shotgun')); + +ALTER TABLE tournament_round_group + ADD COLUMN start_hole smallint + CHECK (start_hole IS NULL OR start_hole BETWEEN 1 AND 18); + +ALTER TABLE session + ADD COLUMN start_mode text NOT NULL DEFAULT 'consecutive' + CHECK (start_mode IN ('consecutive', 'shotgun')); + +ALTER TABLE match + ADD COLUMN start_hole smallint + CHECK (start_hole IS NULL OR start_hole BETWEEN 1 AND 18); diff --git a/app/routers/individual_tournaments.py b/app/routers/individual_tournaments.py index 9634325..51ec918 100644 --- a/app/routers/individual_tournaments.py +++ b/app/routers/individual_tournaments.py @@ -100,6 +100,12 @@ class TournamentRoundCreate(BaseModel): scheduled_at: datetime | None = None tee_interval_minutes: int | None = Field(default=None, gt=0) start_hole: int = Field(default=1, ge=1, le=18) + # Shotgun-start (migrasjon 088, ADR-096) -- 'consecutive' (standard) + # er dagens oppførsel uendret: alle grupper deler DETTE feltet, + # staggert kun på klokkeslett. 'shotgun': alle grupper går ut + # SAMTIDIG, fra hvert sitt starthull (tournament_round_group. + # start_hole) i stedet -- se _compute_group_tee_time. + start_mode: str = Field(default="consecutive", pattern="^(consecutive|shotgun)$") class TournamentRound(BaseModel): @@ -113,12 +119,13 @@ class TournamentRound(BaseModel): scheduled_at: str | None tee_interval_minutes: int | None start_hole: int + start_mode: str _ROUND_COLUMNS = """ tr.id::text, tr.tournament_id::text, tr.sequence, tr.name, tr.hole_config::text, tr.course_id::text, c.name AS course_name, - tr.scheduled_at::text, tr.tee_interval_minutes, tr.start_hole + tr.scheduled_at::text, tr.tee_interval_minutes, tr.start_hole, tr.start_mode """ @@ -162,8 +169,8 @@ async def create_round( WITH inserted AS ( INSERT INTO tournament_round (organization_id, tournament_id, sequence, name, hole_config, - course_id, scheduled_at, tee_interval_minutes, start_hole) - VALUES ($1, $2, $3, $4, $5::hole_scope, $6, $7, $8, $9) + course_id, scheduled_at, tee_interval_minutes, start_hole, start_mode) + VALUES ($1, $2, $3, $4, $5::hole_scope, $6, $7, $8, $9, $10) RETURNING * ) SELECT {_ROUND_COLUMNS} FROM inserted tr JOIN course c ON c.id = tr.course_id @@ -177,6 +184,7 @@ async def create_round( body.scheduled_at, body.tee_interval_minutes, body.start_hole, + body.start_mode, ) return TournamentRound(**dict(row)) @@ -199,6 +207,7 @@ class TournamentRoundUpdate(BaseModel): scheduled_at: datetime | None = None # se TournamentRoundCreate for hvorfor datetime, ikke str tee_interval_minutes: int | None = Field(default=None, gt=0) start_hole: int | None = Field(default=None, ge=1, le=18) + start_mode: str | None = Field(default=None, pattern="^(consecutive|shotgun)$") @router.patch( @@ -777,12 +786,21 @@ def _compute_group_tee_time( tee_interval_minutes: int | None, sequence: int, override: datetime | None, + start_mode: str = "consecutive", ) -> datetime | None: """Samme formel/filosofi som matches.py sin `_compute_tee_time` for lagturneringers økter -- utledet, ikke lagret, med en valgfri - override som vinner (f.eks. én gruppe forsinket).""" + override som vinner (f.eks. én gruppe forsinket). + + Shotgun (migrasjon 088, ADR-096): ALLE grupper går ut SAMTIDIG -- + intervallet er meningsløst i denne modusen (ulikt starthull, ikke + ulikt klokkeslett), så `sequence` ignoreres helt. Kun `scheduled_at` + (felles for hele runden) eller en eksplisitt per-gruppe override + gjelder.""" if override is not None: return override + if start_mode == "shotgun": + return scheduled_at if scheduled_at is None or tee_interval_minutes is None: return None return scheduled_at + timedelta(minutes=(sequence - 1) * tee_interval_minutes) @@ -799,6 +817,10 @@ class RoundGroupOut(BaseModel): sequence: int tee_time: str | None # utledet (eller override), ISO-tekst tee_time_override: str | None + # Kun meningsfullt når runden er 'shotgun' (migrasjon 088, ADR-096) -- + # `None` for en 'consecutive'-runde, som fortsatt bruker rundens EGET + # delte start_hole (uendret siden migrasjon 040). + start_hole: int | None participants: list[RoundGroupParticipant] @@ -811,7 +833,8 @@ class RoundGroupsOut(BaseModel): async def _round_scheduling(conn, tournament_id: str, round_id: str) -> asyncpg.Record: round_row = await conn.fetchrow( - "SELECT scheduled_at, tee_interval_minutes FROM tournament_round WHERE id = $1 AND tournament_id = $2", + "SELECT scheduled_at, tee_interval_minutes, start_mode FROM tournament_round " + "WHERE id = $1 AND tournament_id = $2", round_id, tournament_id, ) @@ -822,7 +845,7 @@ async def _round_scheduling(conn, tournament_id: str, round_id: str) -> asyncpg. async def _fetch_round_groups(conn, round_id: str, round_row) -> RoundGroupsOut: group_rows = await conn.fetch( - "SELECT id::text, sequence, tee_time_override " + "SELECT id::text, sequence, tee_time_override, start_hole " "FROM tournament_round_group WHERE tournament_round_id = $1 ORDER BY sequence", round_id, ) @@ -850,7 +873,11 @@ async def _fetch_round_groups(conn, round_id: str, round_row) -> RoundGroupsOut: groups = [] for g in group_rows: tee_time = _compute_group_tee_time( - round_row["scheduled_at"], round_row["tee_interval_minutes"], g["sequence"], g["tee_time_override"] + round_row["scheduled_at"], + round_row["tee_interval_minutes"], + g["sequence"], + g["tee_time_override"], + round_row["start_mode"], ) groups.append( RoundGroupOut( @@ -858,9 +885,18 @@ async def _fetch_round_groups(conn, round_id: str, round_row) -> RoundGroupsOut: sequence=g["sequence"], tee_time=tee_time.isoformat() if tee_time is not None else None, tee_time_override=g["tee_time_override"].isoformat() if g["tee_time_override"] else None, + start_hole=g["start_hole"], participants=by_group.get(g["id"], []), ) ) + # Visningsrekkefølge (bruker, 2026-08-21): løpende start sorteres etter + # klokkeslett (allerede = sequence-rekkefølgen for 'consecutive', siden + # sequence 1 alltid er tidligst -- ingen endring der), shotgun sorteres + # etter starthull i stedet. Ikke tildelt starthull ennå (None) sorteres + # sist, ikke først -- en uferdig oppsatt gruppe skal ikke fortrenge + # ferdig plasserte grupper i visningen. + if round_row["start_mode"] == "shotgun": + groups.sort(key=lambda gr: (gr.start_hole is None, gr.start_hole)) return RoundGroupsOut(groups=groups, ungrouped=ungrouped) @@ -920,7 +956,7 @@ async def suggest_round_groups( sequence = i // body.group_size + 1 chunk = entries[i : i + body.group_size] tee_time = _compute_group_tee_time( - round_row["scheduled_at"], round_row["tee_interval_minutes"], sequence, None + round_row["scheduled_at"], round_row["tee_interval_minutes"], sequence, None, round_row["start_mode"] ) groups.append( RoundGroupOut( @@ -928,6 +964,12 @@ async def suggest_round_groups( sequence=sequence, tee_time=tee_time.isoformat() if tee_time is not None else None, tee_time_override=None, + # Forslaget dekker KUN sammensetning (hvem spiller med + # hvem) -- starthull for en shotgun-runde er fortsatt et + # eget, manuelt valg organisator gjør etterpå, samme + # "forslag å justere fra, ikke helautomatisk"-prinsipp + # som selve gruppesammensetningen. + start_hole=None, participants=chunk, ) ) @@ -937,6 +979,7 @@ async def suggest_round_groups( class RoundGroupIn(BaseModel): sequence: int = Field(ge=1) tee_time_override: datetime | None = None + start_hole: int | None = Field(default=None, ge=1, le=18) round_participant_ids: list[str] @@ -978,14 +1021,15 @@ async def save_round_groups( group_row = await conn.fetchrow( """ INSERT INTO tournament_round_group - (organization_id, tournament_round_id, sequence, tee_time_override) - VALUES ($1, $2, $3, $4) + (organization_id, tournament_round_id, sequence, tee_time_override, start_hole) + VALUES ($1, $2, $3, $4, $5) RETURNING id """, organization_id, round_id, g.sequence, g.tee_time_override, + g.start_hole, ) if g.round_participant_ids: await conn.execute( diff --git a/app/routers/matches.py b/app/routers/matches.py index e37dfc6..963ea4a 100644 --- a/app/routers/matches.py +++ b/app/routers/matches.py @@ -40,9 +40,15 @@ def _compute_tee_time( tee_interval_minutes: int | None, sequence: int, override: datetime | None, + start_mode: str = "consecutive", ) -> datetime | None: + """Shotgun (migrasjon 088, ADR-096): ALLE matcher i økten går ut + SAMTIDIG -- intervallet/sequence er meningsløst (ulikt starthull, + ikke ulikt klokkeslett), se match.start_hole i stedet.""" if override is not None: return override + if start_mode == "shotgun": + return scheduled_at if scheduled_at is None or tee_interval_minutes is None: return None return scheduled_at + timedelta(minutes=(sequence - 1) * tee_interval_minutes) @@ -64,6 +70,10 @@ class MatchCreate(BaseModel): sequence: int team_a_id: str team_b_id: str + # Kun meningsfullt (og satt av organisator) når økten er 'shotgun' -- + # se _compute_tee_time. `None` for en 'consecutive'-økt, som fortsatt + # bruker øktens EGET delte start_hole (uendret). + start_hole: int | None = Field(default=None, ge=1, le=18) class MatchOut(BaseModel): @@ -76,6 +86,7 @@ class MatchOut(BaseModel): points_side_b: float | None leading_side: str | None tee_time: datetime | None + start_hole: int | None participants: list[MatchParticipantOut] @@ -92,7 +103,7 @@ async def create_match( async with org_connection(organization_id) as conn, translate_db_errors(): session = await conn.fetchrow( """ - SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes + SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes, start_mode FROM session WHERE id = $1 """, session_id, @@ -113,20 +124,25 @@ async def create_match( row = await conn.fetchrow( """ - INSERT INTO match (organization_id, session_id, sequence, team_a_id, team_b_id) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO match (organization_id, session_id, sequence, team_a_id, team_b_id, start_hole) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id::text, sequence, team_a_id::text, team_b_id::text, status_text, points_side_a::float AS points_side_a, - points_side_b::float AS points_side_b, leading_side, tee_time_override + points_side_b::float AS points_side_b, leading_side, tee_time_override, start_hole """, organization_id, session_id, body.sequence, body.team_a_id, body.team_b_id, + body.start_hole, ) tee_time = _compute_tee_time( - session["scheduled_at"], session["tee_interval_minutes"], row["sequence"], row["tee_time_override"] + session["scheduled_at"], + session["tee_interval_minutes"], + row["sequence"], + row["tee_time_override"], + session["start_mode"], ) return MatchOut(**dict(row), tee_time=tee_time, participants=[]) @@ -141,7 +157,7 @@ async def fetch_matches( avslørte matcher, se blind_draw.py).""" session = await conn.fetchrow( """ - SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes + SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes, start_mode FROM session WHERE id = $1 """, session_id, @@ -153,7 +169,7 @@ async def fetch_matches( """ SELECT id::text, sequence, team_a_id::text, team_b_id::text, status_text, points_side_a::float AS points_side_a, - points_side_b::float AS points_side_b, leading_side, tee_time_override + points_side_b::float AS points_side_b, leading_side, tee_time_override, start_hole FROM match WHERE session_id = $1 ORDER BY sequence @@ -203,7 +219,7 @@ async def fetch_matches( ) ) - return [ + out = [ MatchOut( id=m["id"], sequence=m["sequence"], @@ -214,12 +230,23 @@ async def fetch_matches( points_side_b=m["points_side_b"], leading_side=m["leading_side"], tee_time=_compute_tee_time( - session["scheduled_at"], session["tee_interval_minutes"], m["sequence"], m["tee_time_override"] + session["scheduled_at"], + session["tee_interval_minutes"], + m["sequence"], + m["tee_time_override"], + session["start_mode"], ), + start_hole=m["start_hole"], participants=by_match[m["id"]], ) for m in matches ] + # Visningsrekkefølge (bruker, 2026-08-21, ADR-096): løpende sorteres + # etter klokkeslett (allerede = sequence-rekkefølgen for 'consecutive'), + # shotgun sorteres etter starthull -- ikke tildelt (None) sist. + if session["start_mode"] == "shotgun": + out.sort(key=lambda mo: (mo.start_hole is None, mo.start_hole)) + return out @router.get( diff --git a/app/routers/tournaments.py b/app/routers/tournaments.py index beb3a54..ecef1e7 100644 --- a/app/routers/tournaments.py +++ b/app/routers/tournaments.py @@ -899,6 +899,12 @@ class SessionCreate(BaseModel): scheduled_at: datetime | None = None tee_interval_minutes: int | None = None start_hole: int = 1 + # Shotgun-start (migrasjon 088, ADR-096) -- samme mønster som + # tournament_round for individuelle turneringer. 'consecutive' + # (standard) = uendret oppførsel. 'shotgun' = alle matcher i økten + # går ut SAMTIDIG, fra hvert sitt starthull (match.start_hole) i + # stedet for staggert klokkeslett. + start_mode: str = Field(default="consecutive", pattern="^(consecutive|shotgun)$") # KUN meningsfullt (og påkrevd) når format='shamble' -- antall av # sidens individuelle resultater som teller per hull ("beste N av M"). shamble_best_n: int | None = Field(default=None, ge=1) @@ -917,6 +923,7 @@ class SessionOut(BaseModel): scheduled_at: datetime | None tee_interval_minutes: int | None start_hole: int + start_mode: str shamble_best_n: int | None locked_team_ids: list[str] revealed: bool @@ -936,6 +943,7 @@ def _session_out(row, locked: set[str]) -> SessionOut: scheduled_at=row["scheduled_at"], tee_interval_minutes=row["tee_interval_minutes"], start_hole=row["start_hole"], + start_mode=row["start_mode"], shamble_best_n=row["shamble_best_n"], locked_team_ids=sorted(locked), revealed=len(locked) >= 2, @@ -951,7 +959,7 @@ async def _fetch_sessions(conn, tournament_id: str) -> list[SessionOut]: SELECT id::text, sequence, name, format, hole_config::text AS hole_config, course_id::text, points_per_match::float AS points_per_match, allowance_override::text AS allowance_override, scoring_mode, - scheduled_at, tee_interval_minutes, start_hole, shamble_best_n + scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n FROM session WHERE tournament_id = $1 ORDER BY sequence @@ -1008,12 +1016,12 @@ async def create_session( INSERT INTO session (organization_id, tournament_id, sequence, name, format, hole_config, course_id, points_per_match, allowance_override, scoring_mode, - scheduled_at, tee_interval_minutes, start_hole, shamble_best_n) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13, $14) + scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13, $14, $15) RETURNING id::text, sequence, name, format, hole_config::text AS hole_config, course_id::text, points_per_match::float AS points_per_match, allowance_override::text AS allowance_override, scoring_mode, - scheduled_at, tee_interval_minutes, start_hole, shamble_best_n + scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n """, organization_id, tournament_id, @@ -1028,6 +1036,7 @@ async def create_session( body.scheduled_at, body.tee_interval_minutes, body.start_hole, + body.start_mode, body.shamble_best_n, ) return _session_out(row, set()) @@ -1045,6 +1054,7 @@ class SessionUpdate(BaseModel): scheduled_at: datetime | None = None tee_interval_minutes: int | None = None start_hole: int | None = Field(default=None, ge=1, le=18) + start_mode: str | None = Field(default=None, pattern="^(consecutive|shotgun)$") points_per_match: float | None = None allowance_override: dict | None = None course_id: str | None = None @@ -1188,7 +1198,7 @@ async def update_session( SELECT id::text, sequence, name, format, hole_config::text AS hole_config, course_id::text, points_per_match::float AS points_per_match, allowance_override::text AS allowance_override, scoring_mode, - scheduled_at, tee_interval_minutes, start_hole, shamble_best_n + scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n FROM session WHERE id = $1 """, session_id, diff --git a/tests/test_round_groups.py b/tests/test_round_groups.py index 60b6bd8..501535c 100644 --- a/tests/test_round_groups.py +++ b/tests/test_round_groups.py @@ -162,3 +162,56 @@ async def test_group_tee_time_override_wins_over_computed(pool): ) assert result.groups[0].tee_time is not None assert "09:15" in result.groups[0].tee_time # override, IKKE den utledede 10:00 + + +async def test_shotgun_mode_gives_all_groups_the_same_tee_time(pool): + """Migrasjon 088, ADR-096 -- intervallet er meningsløst i shotgun, + alle grupper går ut SAMTIDIG uansett sequence.""" + org_id = await create_org() + tournament_id, round_id, rp_ids = await _setup(org_id, n_players=4) + await update_round( + tournament_id, round_id, + TournamentRoundUpdate( + scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10, start_mode="shotgun", + ), + organization_id=org_id, + ) + + result = await save_round_groups( + tournament_id, round_id, + SaveRoundGroupsIn(groups=[ + RoundGroupIn(sequence=1, start_hole=5, round_participant_ids=[rp_ids[0], rp_ids[1]]), + RoundGroupIn(sequence=2, start_hole=1, round_participant_ids=[rp_ids[2], rp_ids[3]]), + ]), + organization_id=org_id, + ) + # Begge grupper går ut kl 08:00, uavhengig av sequence/intervall. + for g in result.groups: + assert g.tee_time is not None and "08:00" in g.tee_time + + # Vises sortert etter STARTHULL (1 før 5), ikke etter sequence (1 før 2). + assert [g.start_hole for g in result.groups] == [1, 5] + + +async def test_consecutive_mode_still_sorts_by_sequence_not_start_hole(pool): + """Regresjonsvern -- start_hole (uansett verdi, normalt None) skal + IKKE påvirke rekkefølgen når runden fortsatt er 'consecutive'.""" + org_id = await create_org() + tournament_id, round_id, rp_ids = await _setup(org_id, n_players=4) + await update_round( + tournament_id, round_id, + TournamentRoundUpdate(scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10), + organization_id=org_id, + ) + + result = await save_round_groups( + tournament_id, round_id, + SaveRoundGroupsIn(groups=[ + RoundGroupIn(sequence=1, round_participant_ids=[rp_ids[0], rp_ids[1]]), + RoundGroupIn(sequence=2, round_participant_ids=[rp_ids[2], rp_ids[3]]), + ]), + organization_id=org_id, + ) + assert [g.sequence for g in result.groups] == [1, 2] + assert "08:00" in result.groups[0].tee_time + assert "08:10" in result.groups[1].tee_time diff --git a/tests/test_shotgun_start_cup.py b/tests/test_shotgun_start_cup.py new file mode 100644 index 0000000..e65d4de --- /dev/null +++ b/tests/test_shotgun_start_cup.py @@ -0,0 +1,112 @@ +""" +Shotgun-start for Cup-/lagformatet (migrasjon 088, ADR-096) -- samme +mønster som individuelle turneringers utslagsgrupper +(test_round_groups.py), men for session/match. Bruker: "Det må komme +frem om det er Shotgun eller løpende start... Sorteringen av +startlisten gjøres etter utslagsform." +""" + +from app.routers.matches import MatchCreate, create_match, list_matches +from app.routers.tournaments import SessionUpdate, update_session +from app.auth import CurrentUser + +import app.db as app_db + +from tests.conftest import ( + add_membership, + create_course, + create_org, + create_session, + create_team, + create_tournament, + create_user, +) + + +async def _setup(org_id): + owner_id = await create_user() + await add_membership(org_id, owner_id, role="owner") + tournament_id = await create_tournament(org_id, name="Cup-turnering") + course_id = await create_course(org_id, name="Cup Links") + session_id = await create_session(org_id, tournament_id, course_id, format="foursome") + team_a = await create_team(org_id, tournament_id, name="Team Nord") + team_b = await create_team(org_id, tournament_id, name="Team Sør") + return owner_id, tournament_id, session_id, team_a, team_b + + +async def test_shotgun_session_gives_all_matches_the_same_tee_time(pool): + org_id = await create_org() + owner_id, tournament_id, session_id, team_a, team_b = await _setup(org_id) + + await update_session( + session_id, + SessionUpdate(scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10, start_mode="shotgun"), + organization_id=org_id, + ) + + m1 = (await create_match( + session_id, MatchCreate(sequence=1, team_a_id=team_a, team_b_id=team_b, start_hole=5), + organization_id=org_id, + )).id + m2 = (await create_match( + session_id, MatchCreate(sequence=2, team_a_id=team_a, team_b_id=team_b, start_hole=1), + organization_id=org_id, + )).id + + user = CurrentUser(user_id=owner_id) + matches = await list_matches(session_id, organization_id=org_id, user=user) + # Begge matcher går ut kl 08:00, uansett sequence/intervall. + for m in matches: + assert m.tee_time is not None and m.tee_time.isoformat().startswith("2026-08-19T08:00:00") + # Vist sortert etter STARTHULL (1 før 5), ikke sequence (m1 før m2). + assert [m.start_hole for m in matches] == [1, 5] + assert [m.id for m in matches] == [m2, m1] + + +async def test_consecutive_session_still_sorts_by_sequence(pool): + org_id = await create_org() + owner_id, tournament_id, session_id, team_a, team_b = await _setup(org_id) + + await update_session( + session_id, + SessionUpdate(scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10), + organization_id=org_id, + ) + + m1 = (await create_match( + session_id, MatchCreate(sequence=1, team_a_id=team_a, team_b_id=team_b), + organization_id=org_id, + )).id + m2 = (await create_match( + session_id, MatchCreate(sequence=2, team_a_id=team_a, team_b_id=team_b), + organization_id=org_id, + )).id + + user = CurrentUser(user_id=owner_id) + matches = await list_matches(session_id, organization_id=org_id, user=user) + assert [m.id for m in matches] == [m1, m2] + assert matches[0].tee_time.isoformat().startswith("2026-08-19T08:00:00") + assert matches[1].tee_time.isoformat().startswith("2026-08-19T08:10:00") + + +async def test_match_tee_time_override_wins_even_in_shotgun_mode(pool): + org_id = await create_org() + owner_id, tournament_id, session_id, team_a, team_b = await _setup(org_id) + + await update_session( + session_id, + SessionUpdate(scheduled_at="2026-08-19T08:00:00", start_mode="shotgun"), + organization_id=org_id, + ) + match_id = (await create_match( + session_id, MatchCreate(sequence=1, team_a_id=team_a, team_b_id=team_b, start_hole=9), + organization_id=org_id, + )).id + async with app_db.org_connection(org_id) as conn: + await conn.execute( + "UPDATE match SET tee_time_override = '2026-08-19T09:30:00' WHERE id = $1", match_id + ) + + user = CurrentUser(user_id=owner_id) + matches = await list_matches(session_id, organization_id=org_id, user=user) + assert matches[0].tee_time.isoformat().startswith("2026-08-19T09:30:00")