Shotgun-start: backend (migrasjon 088, ADR-096)

Ny start_mode ('consecutive'/'shotgun') på tournament_round + session,
og valgfri start_hole på tournament_round_group + match. Shotgun: alle
grupper/matcher går ut samtidig, sortert etter starthull i stedet for
klokkeslett. Reverserer/utvider migrasjon 084s bevisste "ikke shotgun"
fra 2026-08-18 -- nytt behov oppdaget under utskrift-planlegging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-21 08:59:48 +02:00
parent 0792205c64
commit f1575c9b42
6 changed files with 318 additions and 24 deletions

48
088_shotgun_start.sql Normal file
View file

@ -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);

View file

@ -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), `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(

View file

@ -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(

View file

@ -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,

View file

@ -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

View file

@ -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")