FEATURE_BACKLOG.md — ny rad i "Fundament"-tabellen (📋 planlagt, referer ADR-014), pluss oppdatert rad for oppsett-API (✅) og allowance-raden nå refererer ADR-014. CLAUDE.md — "Neste steg" punkt 1 sier nå eksplisitt at de fire bryterne skal bygges inn samtidig med scoring, ikke bare prosenten.
14 KiB
TeeCup — Arkitektur-beslutningslogg (ADR)
Dette dokumentet er den autoritative kilden til hva som er bestemt og hvorfor. Det leses av mennesker og av AI-modeller (Claude, Gemini) i starten av hver økt. Endre aldri en beslutning uten å legge til en ny ADR som erstatter den — historikken skal bevares.
Status: Levende dokument Sist oppdatert: 2026-07-16
Kontekst
TeeCup er en kommersiell (SaaS) webapplikasjon for å opprette, administrere og
gjennomføre golfturneringer i «Ryder Cup»-format. Den driftes på samme VPS som
teeoff.no, men skal fungere som et isolert økosystem med eget subdomene
(teecup.teeoff.no) og egen database (teecup_db).
Målgruppe: klubber, bedrifter og vennegjenger som arrangerer turneringer over tid.
ADR-001 — Tenant = Organisasjon
Beslutning: Isolasjonsenheten («tenant») er en organisasjon, ikke en turnering.
En organisasjon er et bevisst nøytralt begrep som dekker klubb, bedrift og vennegjeng. Ved å ikke kalle den «klubb» i datamodellen unngår vi refaktorering den dagen første bedriftskunde kommer.
Hierarki:
Organisasjon (tenant — det som isoleres og faktureres)
├── Medlemmer/spillere (gjenbrukbare på tvers av turneringer)
├── Egendefinerte baner
└── Turneringer
├── Lag
└── Matcher
└── Scores
Begrunnelse: En turnering er en hendelse med start og slutt, ikke en kunde. Gjenbrukbare ting (spillere, baner, historikk) må leve over turneringens levetid. Turnering er derfor en entitet inne i en organisasjon.
Konsekvens: tenant_id i alle skjemaer heter organization_id.
ADR-002 — Bruker og organisasjonsmedlemskap er adskilt
Beslutning: Identitet (innlogging) og medlemskap i en organisasjon er to
forskjellige ting. Én user kan ha flere organization_memberships.
Begrunnelse: Samme person spiller ofte både klubbturneringen og jobbturneringen. En bruker må kunne krysse organisasjoner med samme innlogging. Dette er lett å bygge inn fra start og smertefullt å legge til senere.
Konsekvens: Utelukker «database-per-tenant» (se ADR-003), fordi en bruker som krysser organisasjoner da måtte eksistere i flere databaser samtidig.
ADR-003 — Isolasjonsstrategi: Shared schema + Row-Level Security
Beslutning: Én database (teecup_db), delte tabeller, isolert på
organization_id-kolonne, håndhevet av PostgreSQL Row-Level Security (RLS).
Vurderte alternativer:
| Strategi | For | Mot | Valgt |
|---|---|---|---|
Shared schema + organization_id + RLS |
Enkel drift, billig, skalerer til mange org. | Krever disiplin; RLS må settes riktig | Ja |
| Database/schema per tenant | Sterk isolasjon | Tung migrering; bryter med ADR-002 | Nei |
Begrunnelse: RLS flytter isolasjonen fra applikasjonskoden (der én glemt
WHERE organization_id = ... lekker data mellom kunder) ned til databasen, som
håndhever den uansett hva koden gjør. Kombinert med ADR-002 er dette det eneste
praktiske valget.
Konsekvens / oppgave: Hver økt/tilkobling må sette SET app.current_org (eller
tilsvarende) slik at RLS-policyen kan filtrere. Dette må inn i tilkoblingslaget
tidlig, ikke ettermonteres.
ADR-004 — Banedata fra teeoff_db via enveis, lesende API-kontrakt
Beslutning: TeeCup henter offisielle banedata fra hovedplattformen gjennom et veldefinert, lesende API — ikke via direkte databasekobling på tvers.
Begrunnelse: En direkte kobling ville låst TeeCup til hovedplattformens
skjemaendringer, og et brudd ett sted ville tatt ned begge produktene.
teeoff_db er «master» for offisielle banedata; alt brukergenerert innhold
(egendefinerte baner, turneringer, brukere) forblir strengt i teecup_db.
Konsekvens: API-kontrakten mot TeeOff må versjoneres og behandles som en ekstern avhengighet, selv om den kjører på samme server.
ADR-005 — Handicap-motoren er et frittstående, testet bibliotek
Beslutning: All handicap- og score-logikk bygges som en ren Python-modul uten avhengigheter til database, API eller web-rammeverk. Den testes grundig i isolasjon.
Begrunnelse: Dette er produktets hjerte og den mest risikofylte biten. Feil her koster mest troverdighet. Ved å isolere den kan reglene enhetstestes mot kjente fasitverdier uavhengig av resten av systemet.
Konsekvens / viktig presisering: Prosentbaserte «allowances» (f.eks. 75 %, 90 %, 3/4) er konfigurasjon, ikke hardkodet logikk. Motoren vet ikke at «fourball = 90 %»; den mottar tildelingen som parameter. De konkrete standardprosentene per format må verifiseres mot gjeldende WHS/lokale regler før produksjon — de skal ikke antas fra hukommelse.
ADR-006 — Teknologistack
Beslutning:
- Backend: Python + FastAPI (gjenbruker stacken fra TeeOff → enklere vedlikehold).
- Database: PostgreSQL (+ PostGIS der banegeometri trengs).
- Frontend: React (Vite) som PWA, offline-first (Service Workers + IndexedDB).
Begrunnelse: Offline-first er kjernefunksjonalitet, ikke luksus — golfbaner har ofte dårlig mobildekning, og score må kunne registreres uten nett og synkes senere.
ADR-007 — Miksede formater via økter, og spiller-pool
Beslutning: En turnering er en ordnet sekvens av økter (sessions), ikke ett enkelt format. Hver økt bærer format, hullomfang og allowance. Ryder Cup = foursome/18 → fourball/18 → singles/18 er tre økter.
Hvert lag har en pool (team_roster) som kan være større enn antall
matchplasser. Deltakelse avgjøres per match via match_participant. En reserve
er en spiller i poolen uten deltaker-rad i en gitt økt; en spiller kan spille
kun enkelte økter (f.eks. bare singelen).
Begrunnelse: Dette er selve Ryder Cup-strukturen. Å modellere format på turneringsnivå ville gjort miksede formater umulig; å modellere deltakelse på turneringsnivå ville gjort reserver og delvis deltakelse umulig.
Konsekvens: Handicap-snapshot fryses per turnering i team_roster
(handicap_index_snapshot) for reproduserbare resultater.
ADR-008 — 9-hulls-slag: "slagene som faller på 18-hulls-kortet"
Beslutning: For 9-hulls-økter (front/back) brukes spillerens fulle 18-hulls-tildeling, og slagene fordeles over hele 18-hulls stroke index — deretter tas kun de spilte hullene ut. Spilleren mottar slag på de spilte hullene der SI ≤ mottatte slag.
Vurdert alternativ: WHS' formelle 9-hulls course handicap (eget 9-hulls rating, grovt sagt halvparten). Kan gi et litt annet totaltall. Ikke valgt som standard fordi ikke alle baner publiserer 9-hulls-ratinger, og den valgte metoden er den vanlige i vennegjeng-/klubbmatch-spill.
Kritisk implementasjonsdetalj: Man må IKKE sende bare de ni spilte hullene inn
i slagfordelingen med et 18-hulls slagtall — det gir feil ved høye slagtall
(12 slag på back-9 blir da 10 i stedet for riktige 6). Motoren har derfor
allocate_over_played_holes(...) som fordeler over alle 18 og så tar ut de spilte.
Dekket av test test_nine_hole_twelve_strokes_is_six_not_ten.
Konsekvens: tee_rating beholder likevel front/back-omfang, slik at WHS'
alternativ kan tilbys senere som konfig per turnering uten skjemaendring.
ADR-009 — Egen innlogging, uavhengig av teeoff
Beslutning: TeeCup har egne brukerkontoer (app_user), uavhengig av teeoffs
innlogging.
Begrunnelse: TeeCup selges kommersielt til klubber/bedrifter som kanskje aldri har hørt om teeoff.no. Delt innlogging ville vært forvirrende for dem og koblet de to systemenes auth tettere sammen enn ellers ønskelig. Styrker isolasjonen fra ADR-004.
Konsekvens: Egen auth (registrering, Google/magic-link e.l.), egne secrets (jf. åpent spørsmål 1), egen sesjonshåndtering. Kaptein- og tilskuer-roller defineres innenfor TeeCups eget auth-lag.
ADR-010 — Feed krever innlogging (ingen anonym lesesti i v1)
Beslutning: Den «offentlige» runde-feeden er felles for begge lag i turneringen, men bak innlogging. Ingen verdenssynlig lenke i v1.
Begrunnelse: «Offentlig» betyr her «alle innloggede deltakere i turneringen», ikke «hvem som helst med en lenke». Dette fjerner den uautentiserte lesestien og den tunge modereringen (som ellers kreves når innhold er synlig for verden) fra v1.
Konsekvens: Synlighet modelleres som kanal-scope (lag / turnering). En ekte verdenssynlig delingslenke kan legges til senere som egen bryter, med moderering, uten å bygge om.
ADR-011 — Turneringsstruktur: lagformat i v1, bracket som egen fremtidig type
Beslutning: v1 bygges for Ryder Cup-lagformatet (økter → uavhengige matcher → summerte poeng), og låses til nøyaktig to lag.
Begrunnelse: En match/flight er alltid to-sidig. Tre lag ville krevd kryss-oppgjør (A–B, A–C, B–C), nok spillere til å møte to motstandere samtidig, balansert oppsett, og tre-veis blind draw — mye kompleksitet for lite gevinst. To lag er både enklere og riktigere for formatet. Et knockout-bracket (f.eks. 128 spillere, utslagsmatcher, finale + bronsefinale) er en helt egen turneringstype: rundene er avhengige (vinner av match A møter vinner av match B), og krever progresjon mellom matcher, seeding, fripass og bronsegren — noe dagens modell ikke har.
Konsekvens: To-lags-grensen håndheves i app-laget (validering ved turneringsoppsett), ikke med DB-trigger. Match-modellen holdes generell (to sider, vilkårlige lag-referanser), så flere lag eller en knockout-type kan komme senere uten dataomskriving — kjernen (spillere, motor, hull-score, RLS) er typeuavhengig. Knockout er fanget som egen type i FEATURE_BACKLOG.md.
ADR-012 — To scoring-moduser per økt
Beslutning: Hver økt har en scoring_mode: stroke (spillere taster slag per
hull) eller hole_result (registrer bare hvem som vant hullet / delt).
Begrunnelse: Ønsket fra start: kunne føre score enten detaljert (slag) eller raskt (tapp «lag rød vant hullet»). De to modusene har ulik datainngang.
Konsekvens: stroke skriver til hole_score (brutto, motor utleder netto og
hull-resultat). hole_result skriver til ny tabell match_hole_result
(vinnende side per hull, NULL = delt). Begge mater samme
compute_match_state i motoren, så matchstatus beregnes likt uansett modus.
ADR-013 — Blind draw via lås per lag per økt
Beslutning: Lagoppstilling settes skjult; matchene avsløres når BEGGE lag har
låst. Modellert med tabell lineup_lock (én rad per lag per økt).
Begrunnelse: Kjerneønske i Ryder Cup-formatet — kapteinene låser i blinde, og oppgjørene avsløres samtidig.
Konsekvens: match_participant-radene (oppstillingen) opprettes per lag, men
motstanderens side skjules i app-laget til det finnes en lineup_lock for begge
lag. Etter lås kan egen oppstilling ikke endres. Dette er finkornet synlighet
(lag-nivå) og håndheves i app-/spørrelaget, ikke RLS — begge lag ligger i samme
organisasjon (jf. ADR-010).
ADR-014 — Konfigurerbar handicap-pipeline: fire uavhengige brytere
Beslutning: Handicap-anvendelsen i en økt er FIRE uavhengige, valgfrie steg — ikke bare én allowance-prosent:
- Bruk handicap (av/på) — helt av gir scratch-spill.
- Bruk course handicap (av/på) — om slope/rating-justeringen
(
course_handicap_raw) påføres, eller om råhandicap_indexbrukes direkte. - HCP-prosent (allowance-strategien, allerede dekket av ADR-005 /
session.allowance_override). - Bruk matchplay-handicap (av/på) — om resultatet konverteres til relative
slag (
match_play_strokes: beste enhet spiller «av 0», resten får differansen), eller brukes som absolutt Playing Handicap.
Begrunnelse: Skjermbilder fra Golf GameBook (referanseprodukt, delt
2026-07-16) viser nøyaktig denne firedelte bryter-strukturen per runde/format i
deres oppsettsdialog. Deres standard-prosenter (foursome 50 %, better ball/
fourball 90 %, singles 100 %) stemmer eksakt med DEFAULT_MATCHPLAY_ALLOWANCES
i handicap_engine.py — uavhengig bekreftelse på at defaultene er riktige,
slik ADR-005 krever verifisert. Motoren støtter allerede alle fire som atskilte,
komponerbare funksjonskall (course_handicap_raw, en AllowanceStrategy,
match_play_strokes) — hver kan hoppes over uavhengig av de andre — men
API-et/skjemaet eksponerer dem ikke som brytere ennå.
Konsekvens: session.allowance_override (jsonb) skal utvides til å bære
alle fire bryterne, ikke bare prosent, når motor-/scoring-integrasjonen bygges
(se FEATURE_BACKLOG.md). Ingen skjemaendring nødvendig (feltet er allerede
jsonb), men API-lagets validering og motor-kall må håndtere: bruk_handicap = false → hopp over hele kjeden (brutto = netto), bruk_course_handicap = false
→ bruk handicap_index/handicap_index_snapshot direkte uten
slope/rating-justering, bruk_matchplay_handicap = false → bruk absolutt
Playing Handicap i stedet for å kalle match_play_strokes.
Åpne spørsmål (ikke besluttet ennå)
Disse må avklares før eller under de relevante fasene:
- Sesjons-secret: TeeOff lar
PUBLIC_SESSION_SECRETfalle tilbake påJWT_SECRET. TeeCup bør bruke separate, uavhengige secrets. (Sikkerhet) - Cache over flere prosesser: In-memory
dict-cache påapp.statedeles ikke mellom flere workers/containere. Ved skalering trengs Redis. (Skalering) - Prising: Per organisasjon (abonnement) eller per turnering? Påvirker ikke isolasjonsmodellen, men påvirker fakturerings-/kvotemodell.
- Scramble-grensesnitt: Arkitekturen skal ta høyde for formatet; eksakt UI-løsning spesifiseres senere.
- Individuell-vs-delt-ball i
hole_score: Håndheves i app-laget, ikke av databasen (CHECK når ikke opp tilsession.format). Motoren/API-et må passe på at f.eks. et foursome ikke får per-spiller-scorer.
Utviklingsplan (rekkefølge)
- ✅ Land tenant-modell → Organisasjon (ADR-001/002/003)
- ✅ Denne beslutningsloggen (dette dokumentet)
- ✅ Handicap-motor som frittstående, testet bibliotek (ADR-005) — 24 tester, R&A-verifisert
- ✅ Databaseskjema (
001_initial_schema.sql) — RLS, miksede formater, pool - ⏩ Backend-API + regelmotor-integrasjon (neste)
- ⬜ Frontend (admin + score-registrering)
- ⬜ PWA & offline-synk