Ny funksjon (ADR-043, migrasjon 055): organisatorer kan opprette en Order of Merit, lenke individuelle turneringer til den, og få en sesong-sammenlagt rangering regnet ut ved lesing -- fem resultattyper (poeng etter plassering, Stableford-, brutto- og nettoscore-sum, pengeliste), sum/snitt-aggregering, med valgfri behold-N-beste, minimum antall resultater og aldersgrense. Motor i handicap_engine.py (9 nye enhetstester), ny app/routers/order_of_merit.py, og to nye håndkodede frontend-sider under /organizations/[id]/order-of-merit. Eclectic-aggregering og lag-OOM sin faktiske resultatberegning er bevisst ikke bygget denne runden (skjema/CRUD for lag finnes, men leaderboardet avviser kind='team' med en tydelig feilmelding). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
117 lines
6.2 KiB
SQL
117 lines
6.2 KiB
SQL
-- =====================================================================
|
|
-- TeeCup — Order of Merit: sesong-sammenlagt rangering på tvers av
|
|
-- flere individuelle turneringer (migrasjon 055, ADR-043 [OOM])
|
|
-- =====================================================================
|
|
-- Bygges i seks stadier (se plan-loggen 2026-08-04) -- denne migrasjonen
|
|
-- dekker skjemaet for ALLE stadier på én gang (billig å legge til
|
|
-- kolonner nå, dyrere å migrere skjema seks ganger), men selve
|
|
-- BACKEND-logikken bygges stadium for stadium, scratch-verifisert
|
|
-- mellom hvert.
|
|
--
|
|
-- order_of_merit: selve OOM-en. `kind` ('player'|'team') låses etter
|
|
-- opprettelse i app-laget, som GolfBox sin egen tilsvarende låsing.
|
|
-- `result_type` (poeng/stableford/brutto/netto/pengeliste) og
|
|
-- `aggregation_mode` (sum/snitt/eclectic) er de to uavhengige aksene
|
|
-- GolfBox sitt gode mønster besto av -- HVA telles vs. HVORDAN det
|
|
-- summeres. `count_best_n`/`min_results_required`/`birth_year_from/to`
|
|
-- er alle valgfrie grenser (NULL = ingen grense), lagt til fra
|
|
-- stadium 3, men kolonnene finnes fra start.
|
|
-- order_of_merit_link: hvilke individuelle turneringer (ADR-037) som
|
|
-- teller mot en gitt OOM, pluss per-lenke-konfig for resultattyper
|
|
-- som trenger det (poeng-tabell, pengepremie-tabell) -- disse to
|
|
-- JSONB-feltene er NULL med mindre `result_type` faktisk krever dem
|
|
-- (håndhevet i app-laget, ikke en CHECK, siden reglene er
|
|
-- resultattype-avhengige).
|
|
-- order_of_merit_team/_team_member: KUN for kind='team' -- et
|
|
-- sesong-lag satt opp DIREKTE i OOM-en (ikke hentet fra noen
|
|
-- turnering), som summerer/velger-beste-N-av medlemmenes ALLEREDE
|
|
-- beregnede individuelle OOM-resultater. Bevisst IKKE koblet til
|
|
-- `team`/`team_roster` (ADR-011s lagturneringer) -- disse er
|
|
-- strukturelt urelaterte konsepter (ett er en enkelt-turnering-
|
|
-- kamp-side, det andre er en sesong-lang gruppering av spillere).
|
|
--
|
|
-- Bevisst forenklet vs. GolfBox (ikke glemt, kan utvides senere uten
|
|
-- brudd): ingen gjenbrukbart "grense-mal"-system på tvers av flere OOM
|
|
-- (hver OOM har sine egne grenser direkte på raden); ingen "verst N"
|
|
-- eller kombinert beste+verst, kun "behold de N beste".
|
|
-- =====================================================================
|
|
\set ON_ERROR_STOP on
|
|
|
|
CREATE TABLE order_of_merit (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL REFERENCES organization (id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
kind text NOT NULL CHECK (kind IN ('player', 'team')),
|
|
result_type text NOT NULL CHECK (result_type IN ('points', 'stableford', 'gross', 'net', 'money')),
|
|
aggregation_mode text NOT NULL DEFAULT 'sum' CHECK (aggregation_mode IN ('sum', 'average', 'eclectic')),
|
|
count_best_n smallint CHECK (count_best_n IS NULL OR count_best_n >= 1),
|
|
min_results_required smallint CHECK (min_results_required IS NULL OR min_results_required >= 1),
|
|
birth_year_from smallint,
|
|
birth_year_to smallint,
|
|
public_visible boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (organization_id, id),
|
|
-- Eclectic krever per-hull-granularitet -- poeng/pengeliste har ingen
|
|
-- "beste hull" å hente (samme begrensning GolfBox selv håndhever).
|
|
CHECK (aggregation_mode <> 'eclectic' OR result_type IN ('stableford', 'gross', 'net'))
|
|
);
|
|
CREATE INDEX ON order_of_merit (organization_id);
|
|
|
|
CREATE TABLE order_of_merit_link (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL,
|
|
order_of_merit_id uuid NOT NULL REFERENCES order_of_merit (id) ON DELETE CASCADE,
|
|
tournament_id uuid NOT NULL,
|
|
-- Kun result_type='points': liste av heltall, indeks 0 = poeng for
|
|
-- 1. plass, indeks 1 = 2. plass, osv.
|
|
points_table jsonb,
|
|
-- Kun result_type='money': premiepott + utbetaling per plassering.
|
|
money_pool_total numeric(10, 2),
|
|
money_payout_table jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
FOREIGN KEY (organization_id, tournament_id) REFERENCES tournament (organization_id, id) ON DELETE CASCADE,
|
|
UNIQUE (order_of_merit_id, tournament_id)
|
|
);
|
|
CREATE INDEX ON order_of_merit_link (organization_id, order_of_merit_id);
|
|
|
|
CREATE TABLE order_of_merit_team (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL,
|
|
order_of_merit_id uuid NOT NULL REFERENCES order_of_merit (id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (order_of_merit_id, name)
|
|
);
|
|
CREATE INDEX ON order_of_merit_team (organization_id, order_of_merit_id);
|
|
|
|
CREATE TABLE order_of_merit_team_member (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL,
|
|
order_of_merit_team_id uuid NOT NULL REFERENCES order_of_merit_team (id) ON DELETE CASCADE,
|
|
player_id uuid NOT NULL REFERENCES player (id) ON DELETE CASCADE,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (order_of_merit_team_id, player_id)
|
|
);
|
|
CREATE INDEX ON order_of_merit_team_member (organization_id, order_of_merit_team_id);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Row-Level Security (samme org_isolation-policy-mønster som 040/053)
|
|
-- ---------------------------------------------------------------------
|
|
DO $$
|
|
DECLARE t text;
|
|
BEGIN
|
|
FOREACH t IN ARRAY ARRAY[
|
|
'order_of_merit', 'order_of_merit_link',
|
|
'order_of_merit_team', 'order_of_merit_team_member'
|
|
]
|
|
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 $$;
|