diff --git a/040_individual_tournaments.sql b/040_individual_tournaments.sql new file mode 100644 index 0000000..26be02b --- /dev/null +++ b/040_individual_tournaments.sql @@ -0,0 +1,204 @@ +-- ===================================================================== +-- TeeCup — individuelle/flerrunde-turneringer: grunnstruktur (migrasjon +-- 040, ADR-037) +-- ===================================================================== +-- Realiserer ADR-037 sine fire beslutninger. Rent additivt — ingen +-- eksisterende tabell/rad endres i betydning, kun `tournament` som får +-- to nye, nullable-med-trygg-default kolonner. +-- +-- Beslutning A: NY, parallell org-scopet tabellstruktur — IKKE en +-- utvidelse av de frittstående rundetabellene (round/round_participant/ +-- round_hole, ADR-033). Disse nye tabellene er RLS-beskyttet på +-- organization_id, som alt annet turnering-relatert. +-- Beslutning B: SAMME `tournament`-tabell, ny `format_type`-diskriminator +-- ('team' | 'individual', default 'team' — alle eksisterende rader +-- forblir 'team' uendret). `team`/`team_roster` blir ganske enkelt +-- ubrukte for format_type='individual', håndhevet i app-laget (samme +-- mønster som ADR-011s to-lags-grense), ikke en tabellomfattende CHECK. +-- Beslutning C: Flerrunde via ny `tournament_round`-tabell (samme rolle +-- som `session` har for lagturneringer), pekende til EKSISTERENDE +-- org-scopet `course` (ingen snapshot — se ADR-037s egen begrunnelse: +-- snapshot-behovet i ADR-033 kom av at frittstående runder leser en +-- ekstern, ikke-org-kontrollert kilde, det gjelder ikke her). Ny +-- `tournament_participant` (samme rolle som `team_roster`, fryser +-- handicap_index_snapshot ved uttak — ADR-007-prinsippet). +-- Beslutning D: Rå brutto slag lagres (`tournament_round_hole`, +-- individuell ball — samme grunnform som hole_score for singles/ +-- fourball), OG et ferdig utregnet resultat caches per runde per +-- deltaker (`tournament_round_score` — samme mønster som +-- match.status_text/points_side_a/b). Sammenlagt resultat over flere +-- runder summeres VED LESING (samme prinsipp som +-- tournament_standings-viewet for lagturneringer), ikke en egen +-- tredje cache-tabell. +-- +-- BEVISST UTENFOR DENNE MIGRASJONEN (se ADR-037 "hva denne ADR-en IKKE +-- avgjør ennå"): de fem konkrete formatenes egne poengregler +-- (Københavner/High-low-high/Robbins/Try all/Flaggturnering) og Order of +-- Merit (sesong-sammenlagt på tvers av FLERE turneringer). Dette +-- skjemaet dekker kun rene, per-spiller-uavhengige scoringsmetoder +-- (bruttoslagspill/nettoslagspill/Stableford) — formater som krever +-- paring PÅ TVERS av spillere per hull (Københavner m.fl.) trenger sin +-- egen, senere design-runde oppå denne grunnstrukturen. +-- ===================================================================== +\set ON_ERROR_STOP on + +-- --------------------------------------------------------------------- +-- 1. tournament: type-diskriminator + scoringsmetode (Beslutning B) +-- --------------------------------------------------------------------- +ALTER TABLE tournament ADD COLUMN format_type text NOT NULL DEFAULT 'team' + CHECK (format_type IN ('team', 'individual')); + +-- Hvilken motorfunksjon (handicap_engine.py) som regner cachet poeng fra +-- de rå slagene i tournament_round_hole. Nullable -- en individuell +-- turnering kan opprettes som 'draft' før scoringsmetoden er bestemt; +-- app-laget krever den satt før en runde kan ta imot score (samme +-- "krev X før Y virker, håndhevet i app-laget"-mønster som f.eks. hull- +-- data for egendefinerte baner andre steder i skjemaet). +ALTER TABLE tournament ADD COLUMN scoring_method text + CHECK (scoring_method IS NULL OR scoring_method IN ('stroke_gross', 'stroke_net', 'stableford')); + +-- --------------------------------------------------------------------- +-- 2. tournament_round: én runde/dag i en individuell turnering +-- (samme rolle som `session` har for lagturneringer) +-- --------------------------------------------------------------------- +CREATE TABLE tournament_round ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id uuid NOT NULL, + tournament_id uuid NOT NULL, + sequence smallint NOT NULL, -- rekkefølge i turneringen (1,2,3...) + name text, -- f.eks. "Runde 1 — fredag" + hole_config hole_scope NOT NULL DEFAULT 'full_18', + course_id uuid NOT NULL, -- org-ens EGEN banedata, ingen snapshot + scheduled_at timestamptz, + tee_interval_minutes smallint CHECK (tee_interval_minutes IS NULL OR tee_interval_minutes > 0), + start_hole smallint NOT NULL DEFAULT 1 CHECK (start_hole BETWEEN 1 AND 18), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + FOREIGN KEY (organization_id, tournament_id) + REFERENCES tournament(organization_id, id) ON DELETE CASCADE, + FOREIGN KEY (organization_id, course_id) + REFERENCES course(organization_id, id) ON DELETE RESTRICT, + UNIQUE (organization_id, id), + UNIQUE (tournament_id, sequence) +); + +-- --------------------------------------------------------------------- +-- 3. tournament_participant: spiller-pool for EN individuell turnering +-- (samme rolle som `team_roster`, men uten noe lag) +-- --------------------------------------------------------------------- +CREATE TABLE tournament_participant ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id uuid NOT NULL, + tournament_id uuid NOT NULL, + player_id uuid NOT NULL, + handicap_index_snapshot numeric(4,1), -- fryst ved uttak, ADR-007-prinsippet + created_at timestamptz NOT NULL DEFAULT now(), + FOREIGN KEY (organization_id, tournament_id) + REFERENCES tournament(organization_id, id) ON DELETE CASCADE, + FOREIGN KEY (organization_id, player_id) + REFERENCES player(organization_id, id) ON DELETE RESTRICT, + UNIQUE (organization_id, id), + UNIQUE (tournament_id, player_id) -- én spiller én gang i feltet +); + +-- --------------------------------------------------------------------- +-- 4. tournament_round_participant: hvilken tee + hvilket beregnet +-- handicap en deltaker har for ÉN GITT runde (banen/tee kan variere +-- runde til runde selv om spilleren er den samme -- samme rolle som +-- match_participant sin tee_id/course_handicap/playing_handicap) +-- --------------------------------------------------------------------- +CREATE TABLE tournament_round_participant ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id uuid NOT NULL, + tournament_round_id uuid NOT NULL, + tournament_participant_id uuid NOT NULL, + tee_id uuid NOT NULL, + -- Cachede handicap-felt (kilde: motoren, ut fra indeks-snapshot + tee-rating). + course_handicap smallint, + playing_handicap smallint, -- kun relevant for scoring_method='stroke_net'/'stableford' + created_at timestamptz NOT NULL DEFAULT now(), + FOREIGN KEY (organization_id, tournament_round_id) + REFERENCES tournament_round(organization_id, id) ON DELETE CASCADE, + FOREIGN KEY (organization_id, tournament_participant_id) + REFERENCES tournament_participant(organization_id, id) ON DELETE RESTRICT, + FOREIGN KEY (organization_id, tee_id) + REFERENCES tee(organization_id, id) ON DELETE RESTRICT, + UNIQUE (organization_id, id), + UNIQUE (tournament_round_id, tournament_participant_id) +); + +-- --------------------------------------------------------------------- +-- 5. tournament_round_hole: rå brutto slag per hull (kilde-sannhet, +-- samme prinsipp som hole_score/round_hole -- netto/Stableford er +-- ALLTID avledet, aldri lagret som kilde) +-- --------------------------------------------------------------------- +CREATE TABLE tournament_round_hole ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id uuid NOT NULL, + tournament_round_participant_id uuid NOT NULL, + hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18), + gross_strokes smallint NOT NULL CHECK (gross_strokes BETWEEN 1 AND 20), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + FOREIGN KEY (organization_id, tournament_round_participant_id) + REFERENCES tournament_round_participant(organization_id, id) ON DELETE CASCADE, + UNIQUE (tournament_round_participant_id, hole_number) +); + +-- --------------------------------------------------------------------- +-- 6. tournament_round_score: FERDIG UTREGNET resultat, cachet per runde +-- per deltaker (samme mønster som match.status_text/points_side_a/b +-- -- motoren regner på nytt ved hver hull-innsending, SQL summerer +-- bare de cachede tallene ved lesing). Alle tre kolonner fylles +-- uansett scoring_method (billig å ha alle tre klare -- lar +-- leaderboardet bytte visning uten omregning). +-- --------------------------------------------------------------------- +CREATE TABLE tournament_round_score ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id uuid NOT NULL, + tournament_round_id uuid NOT NULL, + tournament_participant_id uuid NOT NULL, + holes_played smallint NOT NULL DEFAULT 0, + gross_total smallint, + net_total smallint, + stableford_points smallint, + updated_at timestamptz NOT NULL DEFAULT now(), + FOREIGN KEY (organization_id, tournament_round_id) + REFERENCES tournament_round(organization_id, id) ON DELETE CASCADE, + FOREIGN KEY (organization_id, tournament_participant_id) + REFERENCES tournament_participant(organization_id, id) ON DELETE CASCADE, + UNIQUE (tournament_round_id, tournament_participant_id) +); + +-- --------------------------------------------------------------------- +-- 7. Indekser (RLS-predikat og FK-oppslag, samme konvensjon som 001) +-- --------------------------------------------------------------------- +CREATE INDEX ON tournament_round (organization_id, tournament_id); +CREATE INDEX ON tournament_participant (organization_id, tournament_id); +CREATE INDEX ON tournament_round_participant (organization_id, tournament_round_id); +CREATE INDEX ON tournament_round_participant (organization_id, tournament_participant_id); +CREATE INDEX ON tournament_round_hole (organization_id, tournament_round_participant_id); +CREATE INDEX ON tournament_round_score (organization_id, tournament_round_id); +CREATE INDEX ON tournament_round_score (organization_id, tournament_participant_id); + +-- --------------------------------------------------------------------- +-- 8. Row-Level Security (samme org_isolation-policy-mønster som 001) +-- --------------------------------------------------------------------- +DO $$ +DECLARE t text; +BEGIN + FOREACH t IN ARRAY ARRAY[ + 'tournament_round', 'tournament_participant', + 'tournament_round_participant', 'tournament_round_hole', + 'tournament_round_score' + ] + LOOP + EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', t); + EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY;', t); + EXECUTE format($p$ + CREATE POLICY org_isolation ON %I + USING (organization_id = current_setting('app.current_org', true)::uuid) + WITH CHECK (organization_id = current_setting('app.current_org', true)::uuid); + $p$, t); + END LOOP; +END $$; diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 815c704..0e0b830 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -2521,7 +2521,7 @@ gjennomgang av blokk-forslaget over først): --- -## Venner, kategorisert deling av runder, og tiered personsøk — ✅ FASE 1 (venner-kjernen) FERDIG, ✅ deler av FASE 3 (søk+legg til medspiller, skriv for hele flighten) BYGGET 2026-07-26, fase 2 (rundevisibilitet) fortsatt kun designet +## Venner, kategorisert deling av runder, og tiered personsøk — ✅ HELT FERDIG, alle tre faser bygget og live (fase 1: 2026-07-25, fase 2 rundevisibilitet: 2026-07-28, fase 3 delvis: 2026-07-26) Reist av brukeren samme runde som dashbord-forslaget over. Fullt design skrevet som **ADR-036** i ARCHITECTURE_DECISIONS.md — se der for @@ -2553,18 +2553,23 @@ fritekst) gjør nå "samme klubb"-rangeringen i søket pålitelig — et eksakt strengmatch fungerer nå, noe det ikke ville gjort med den gamle fritekst-versjonen. -**Foreslått byggerekkefølge (IKKE bekreftet), tre uavhengig leverbare -faser:** -1. Venner-kjernen (vennskap, kategorisering, `/people/search`, ny - `/friends`-side) — leverbar og nyttig helt alene. -2. Rundevisibilitet (visibility-valg ved rundestart, gatede - lese-endepunkter, sanntids-livevisning for venner/offentlighet). -3. Ekte medspillere (ikke bare gjester) — utvider +**Byggerekkefølge, alle tre uavhengig leverbare faser — ✅ ALLE FERDIG:** +1. **✅ Venner-kjernen** (vennskap, kategorisering, `/people/search`, ny + `/friends`-side) — live 2026-07-25, se "Fase 1"-underseksjon under. +2. **✅ Rundevisibilitet** (visibility-valg ved rundestart, gatede + lese-endepunkter, sanntids-livevisning for venner/offentlighet) — + bygget, scratch-/browserverifisert og live 2026-07-28 + (`round.visibility_mode`, migrasjon 032, `_can_view_round`/nye + `/public/rounds/*`-endepunkter, ny `/watch/[id]`-side og + `/my-friends/[id]`-profilside). Se CLAUDE.md-status 2026-07-28 for + full detalj (191 automatiserte sjekker + full nettleser-gjennomgang + med tre reelle nettleserkontekster). +3. **✅ Ekte medspillere** (ikke bare gjester) — utvider `POST /rounds/{id}/participants` til å godta et søkt `user_id`. - **✅ DENNE DELEN BYGGET 2026-07-26** (se egen "Fase 3 (delvis)"- - underseksjon under Fase 1) — søk+legg til + "skriv for hele - flighten" er live. `list_rounds`/`RoundOut` sine viewer-relative - `my_*`-felt (se under) er del av denne leveransen. + Bygget 2026-07-26 (se egen "Fase 3 (delvis)"-underseksjon under + Fase 1) — søk+legg til + "skriv for hele flighten" er live. + `list_rounds`/`RoundOut` sine viewer-relative `my_*`-felt (se under) + er del av denne leveransen. Sletting av runden og bane-/utslagsbytte forblir eier-eksklusivt (bekreftet, se ADR-036).