2026-07-16 07:18:01 +02:00
|
|
|
|
# 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-15
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-16 07:26:04 +02:00
|
|
|
|
## 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).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-16 07:18:01 +02:00
|
|
|
|
## Åpne spørsmål (ikke besluttet ennå)
|
|
|
|
|
|
|
|
|
|
|
|
Disse må avklares før eller under de relevante fasene:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Sesjons-secret:** TeeOff lar `PUBLIC_SESSION_SECRET` falle tilbake på
|
|
|
|
|
|
`JWT_SECRET`. TeeCup bør bruke separate, uavhengige secrets. *(Sikkerhet)*
|
|
|
|
|
|
2. **Cache over flere prosesser:** In-memory `dict`-cache på `app.state` deles ikke
|
|
|
|
|
|
mellom flere workers/containere. Ved skalering trengs Redis. *(Skalering)*
|
|
|
|
|
|
3. **Prising:** Per organisasjon (abonnement) eller per turnering? Påvirker ikke
|
|
|
|
|
|
isolasjonsmodellen, men påvirker fakturerings-/kvotemodell.
|
|
|
|
|
|
4. **Scramble-grensesnitt:** Arkitekturen skal ta høyde for formatet; eksakt
|
|
|
|
|
|
UI-løsning spesifiseres senere.
|
|
|
|
|
|
5. **Individuell-vs-delt-ball i `hole_score`:** Håndheves i app-laget, ikke av
|
|
|
|
|
|
databasen (CHECK når ikke opp til `session.format`). Motoren/API-et må passe
|
|
|
|
|
|
på at f.eks. et foursome ikke får per-spiller-scorer.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Utviklingsplan (rekkefølge)
|
|
|
|
|
|
|
|
|
|
|
|
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
|
|
|
|
|
2. ✅ Denne beslutningsloggen (dette dokumentet)
|
|
|
|
|
|
3. ✅ Handicap-motor som frittstående, testet bibliotek (ADR-005) — 24 tester, R&A-verifisert
|
|
|
|
|
|
4. ✅ Databaseskjema (`001_initial_schema.sql`) — RLS, miksede formater, pool
|
|
|
|
|
|
5. ⏩ Backend-API + regelmotor-integrasjon (neste)
|
|
|
|
|
|
6. ⬜ Frontend (admin + score-registrering)
|
|
|
|
|
|
7. ⬜ PWA & offline-synk
|