-- ===================================================================== -- TeeCup — frittstående rundeføring med detaljert statistikk -- (migrasjon 020, ADR-033) -- ===================================================================== -- Frittstående runder er eid av en BRUKER (`app_user.id`), ikke en -- organisasjon (Beslutning A) -- INGEN RLS på disse tabellene. Samme -- allerede etablerte mønster som personlig profil (015)/HCP-historikk -- (018)/sekundær e-post (017): `plain_connection()` (ingen -- `app.current_org`), autorisasjon håndheves eksplisitt i app-laget med -- `WHERE owner_user_id = $1` i hver spørring. -- -- Banedata (Beslutning C, bekreftet med bruker): offisielle teeoff-baner -- slås opp LIVE ved behov (ingen lokal kopi/import, kun en referanse + -- et navne-snapshot for visning). Egendefinerte baner som ikke finnes i -- teeoff havner i en NY, GLOBAL banekatalog (`personal_course` + -- tilhørende hull/utslag/rating-tabeller) -- delt på tvers av ALLE -- TeeCup-brukere (ikke org-scopet som den eksisterende `course`-tabellen), -- med søk-før-opprett tenkt håndtert i API-/frontend-laget for å begrense -- duplikater (ikke håndhevet i skjemaet). -- -- `personal_course_tee_rating` har BEVISST ingen `scope`-kolonne (ulikt -- den org-scopede `tee_rating`) -- Beslutning G sin Net-Par-tilnærming -- for uspilte hull gjør at frittstående runder alltid regnes som en -- 18-hulls-ekvivalent gjennom ÉN formel, uansett om 9 eller 18 hull -- faktisk ble spilt. Ingen egen front_9/back_9-rating trengs derfor her. -- -- Hver runde snapshotter par/stroke-index PER SPILT HULL på selve -- `round_hole`-raden (samme reproduserbarhets-prinsipp som ADR-007 sin -- handicap-snapshot) -- en fremtidig endring i teeoff sin banedata, eller -- en redigering av en `personal_course`, skal ALDRI endre en allerede -- spilt rundes tall retroaktivt. -- -- Statistikk-modellen (Beslutning B): et fast sett navngitte felt per -- hull, ikke fri slag-for-slag-logging. GIR er bevisst IKKE en egen -- lagret kolonne -- den er alltid DERIVERBAR ved lesing -- (`approach_result = 'hit' AND (score - putts) <= par - 2`), se -- ADR-033. -- -- Bevisst v1-avgrensning: en `round` er kun synlig/redigerbar for sin -- `owner_user_id` -- en lenket deltaker (`round_participant.user_id` -- satt) får IKKE egen tilgang til runden i denne runden (notert som -- åpent punkt i ADR-033, ikke løst her). -- ===================================================================== \set ON_ERROR_STOP on -- --------------------------------------------------------------------- -- Global banekatalog for egendefinerte (ikke-teeoff) baner -- --------------------------------------------------------------------- CREATE TABLE personal_course ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_by_user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE personal_course_hole ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), personal_course_id uuid NOT NULL REFERENCES personal_course(id) ON DELETE CASCADE, hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18), par smallint NOT NULL CHECK (par BETWEEN 3 AND 6), stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18), UNIQUE (personal_course_id, hole_number), UNIQUE (personal_course_id, stroke_index) ); CREATE TABLE personal_course_tee ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), personal_course_id uuid NOT NULL REFERENCES personal_course(id) ON DELETE CASCADE, name text NOT NULL, UNIQUE (personal_course_id, name) ); CREATE TABLE personal_course_tee_rating ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), personal_course_tee_id uuid NOT NULL REFERENCES personal_course_tee(id) ON DELETE CASCADE, -- Kun m/f, ikke 'x' -- en WHS-rating er alltid for ett bestemt kjønn -- (samme presedens som org-scopet tee_rating, ADR-029). gender text NOT NULL CHECK (gender IN ('m', 'f')), course_rating numeric(4,1) NOT NULL, slope_rating smallint NOT NULL CHECK (slope_rating BETWEEN 55 AND 155), par smallint NOT NULL, UNIQUE (personal_course_tee_id, gender) ); -- --------------------------------------------------------------------- -- Selve runden -- --------------------------------------------------------------------- CREATE TABLE round ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, course_source text NOT NULL CHECK (course_source IN ('teeoff', 'custom')), -- 'teeoff': live oppslag, kun referanse + navn-snapshot lagres. teeoff_facility_slug text, teeoff_course_id text, -- 'custom': ekte FK til den globale katalogen over. personal_course_id uuid REFERENCES personal_course(id), CHECK ( (course_source = 'teeoff' AND teeoff_facility_slug IS NOT NULL AND teeoff_course_id IS NOT NULL AND personal_course_id IS NULL) OR (course_source = 'custom' AND personal_course_id IS NOT NULL AND teeoff_facility_slug IS NULL AND teeoff_course_id IS NULL) ), -- Snapshot for visning uten et nytt live-oppslag hver gang runden åpnes. course_name_snapshot text NOT NULL, tee_name_snapshot text NOT NULL, played_at date NOT NULL, -- Fritt starthull (Beslutning E) -- 9/18 under er kun et UI-standardvalg, -- IKKE håndhevet (spilleren kan avslutte etter et vilkårlig antall hull). start_hole smallint NOT NULL DEFAULT 1 CHECK (start_hole BETWEEN 1 AND 18), holes_planned smallint NOT NULL DEFAULT 18 CHECK (holes_planned IN (9, 18)), completed_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX ON round (owner_user_id); -- --------------------------------------------------------------------- -- Deltakere i runden -- eieren selv ELLER andre i flighten (Beslutning D) -- --------------------------------------------------------------------- CREATE TABLE round_participant ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), round_id uuid NOT NULL REFERENCES round(id) ON DELETE CASCADE, -- Nøyaktig én av user_id/guest_name -- en ekte TeeCup-bruker som -- spiller med, ELLER et rent navn uten konto (samme prinsipp som -- org-scopet `player` uten `user_id`). user_id uuid REFERENCES app_user(id), guest_name text, CHECK ( ((user_id IS NOT NULL)::int + (guest_name IS NOT NULL)::int) = 1 ), is_owner boolean NOT NULL DEFAULT false, -- Snapshot ved tilføyelse (samme reproduserbarhets-prinsipp som ADR-007 -- sin handicap-snapshot) -- en senere profilendring skal ikke endre en -- allerede spilt rundes tall retroaktivt. gender text NOT NULL CHECK (gender IN ('m', 'f', 'x')), handicap_index_snapshot numeric(4,1), -- NULL = ingen HCP-sporing for denne deltakeren course_handicap_snapshot smallint, -- Fylles ved fullføring (ADR-033 Beslutning F/G) -- kun sant/satt når -- minimums-/gyldighetskravene er oppfylt. counts_for_handicap boolean NOT NULL DEFAULT false, score_differential numeric(4,1), created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX ON round_participant (round_id); CREATE INDEX ON round_participant (user_id); -- Maks én markert eier per runde. CREATE UNIQUE INDEX round_participant_one_owner ON round_participant (round_id) WHERE is_owner; -- --------------------------------------------------------------------- -- Hull-for-hull -- rating-snapshot OG statistikk, per deltaker -- --------------------------------------------------------------------- CREATE TABLE round_hole ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), round_participant_id uuid NOT NULL REFERENCES round_participant(id) ON DELETE CASCADE, hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18), -- Rating-snapshot (par/stroke-index på TIDSPUNKTET runden ble spilt) -- -- se moduldoc-kommentaren øverst om hvorfor dette IKKE er en live-referanse. par smallint NOT NULL CHECK (par BETWEEN 3 AND 6), stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18), played boolean NOT NULL DEFAULT false, score smallint CHECK (score IS NULL OR score BETWEEN 1 AND 20), putts smallint CHECK (putts IS NULL OR putts BETWEEN 0 AND 10), club_off_tee text, -- Kun meningsfullt på par 4/5 (håndheves i app-laget, ikke her). tee_shot_result text CHECK (tee_shot_result IS NULL OR tee_shot_result IN ('fairway', 'left', 'right')), -- "Innspillsslaget" = siste slag før første putt, uansett hullets par -- (ADR-033 Beslutning B) -- GIR er DERIVERT herfra ved lesing, ikke en -- egen lagret kolonne: approach_result='hit' AND (score-putts) <= par-2. approach_result text CHECK (approach_result IS NULL OR approach_result IN ('hit', 'long', 'short', 'left', 'right')), chip_count smallint CHECK (chip_count IS NULL OR chip_count >= 0), bunker_shot_count smallint CHECK (bunker_shot_count IS NULL OR bunker_shot_count >= 0), penalty_strokes smallint CHECK (penalty_strokes IS NULL OR penalty_strokes >= 0), first_putt_distance_m numeric(4,1) CHECK (first_putt_distance_m IS NULL OR first_putt_distance_m >= 0), UNIQUE (round_participant_id, hole_number) ); -- --------------------------------------------------------------------- -- Rettigheter -- INGEN RLS her (Beslutning A). teecup_app filtrerer selv -- med WHERE owner_user_id = $1 (round) / gjennom round_id (de to andre) i -- hver spørring, samme mønster som handicap_history/user_secondary_email. -- --------------------------------------------------------------------- GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course TO teecup_app; GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_hole TO teecup_app; GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_tee TO teecup_app; GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_tee_rating TO teecup_app; GRANT SELECT, INSERT, UPDATE, DELETE ON round TO teecup_app; GRANT SELECT, INSERT, UPDATE, DELETE ON round_participant TO teecup_app; GRANT SELECT, INSERT, UPDATE, DELETE ON round_hole TO teecup_app;