Arkitektur
Systemoversikt
┌─────────────────────────────────────────────────────────────┐
│ Brukere (Medlemmer/Admins) │
└────────────┬─────────────────────────────────┬──────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Mobil (PWA) │ │ Nettleser │
│ iOS/Android │ │ PC/Mobil │
└────────┬────────┘ └────────┬────────┘
│ │
└────────────┬────────────────────┘
▼
┌─────────────────────────┐
│ Azure Static Web App │
│ (Frontend: Vue 3 PWA) │
└────────────┬────────────┘
│ HTTP/REST
▼
┌─────────────────────────┐
│ Azure Functions │
│ (API: TypeScript) │
│ - Autentisering │
│ - Forretningslogikk │
│ - Hastighetsbegrensning│
└────────────┬────────────┘
│
▼
┌────────────────────────────┐
│ Azure Cosmos DB │
│ (NoSQL-database) │
│ - Brukere & Organisasjoner│
│ - Arrangementer & │
│ Bestillinger │
│ - Tildelinger & Salg │
└────────────────────────────┘
Eksterne tjenester:
┌──────────────┐
│ Resend │ ← Magic Link e-poster (Valgfri e-posttjeneste)
└──────────────┘Produksjonsovervåking
Azure Functions sender innebygd backendtelemetri til arbeidsområdebasert Application Insights og Log Analytics. Frontenden laster ikke inn en egen telemetri-SDK. Dette holder betaavtrykket lite og retter overvåkingen mot API-feil, autentisering, invitasjoner, bestillingsoverganger og kapasitetsbrudd.
Produksjonsgrunnlaget omfatter:
- Sampling av rutinemessig vertstelemetri, begrenset til fem elementer per sekund per funksjonsinstans
- Advarsels- og feilhendelser med stabile navn, alvorlighetsgrad og forespørselsavgrenset korrelasjon
- 31 dagers effektiv oppbevaring for Application Insights-telemetri i det tilknyttede Log Analytics-arbeidsområdet
- En grense på 100 MB per dag for Application Insights med tidlig varsel ved 80 MB
- Varsler som bare vises i portalen for mislykkede forespørsler, unntakstopper, mislykkede avhengigheter og høyt inntak
- Telemetri etter beste evne som aldri avbryter billett- eller autentiseringsflyter
Application Insights-ressursen setter RetentionInDays til 30 fordi komponent-API-et godtar faste verdier. Innstillingen oppretter ikke en egen effektiv oppbevaringsperiode for arbeidsområdebasert telemetri. Innstillingen på 31 dager i det tilknyttede arbeidsområdet styrer de lagrede dataene.
Personverngrensene er en del av telemetrikontrakten. Ikke logg rå innloggingslenker, koder, e-postadresser, autorisasjonshoder, forespørselskropper, fullstendige stabile identifikatorer eller kundedata fra en annen organisasjon. Strukturerte feilhendelser maskerer personopplysninger og forkorter identifikatorer som brukes til diagnose. Application Insights maskerer det plattforminnsamlede klient-IP-feltet, men maskeringen fjerner ikke verdier som skrives i egendefinerte spormeldinger. Dagens egendefinerte spor kan derfor inneholde klient-IP-data og krever samme tilgangs- og oppbevaringskontroller som andre personopplysninger. Plattformen deaktiverer også øyeblikksbilder, SQL-kommandotekst og utvidede HTTP-utløserdetaljer.
Teknologistabel
| Lag | Teknologi |
|---|---|
| Frontend | Vue 3 + Vite + Tailwind CSS + PWA + vue-i18n |
| Backend | Azure Functions (Node.js / TypeScript) |
| Database | Azure Cosmos DB (Gratis tier) |
| Hosting | Azure Static Web Apps (Gratis tier) |
| Infrastruktur | Bicep (Infrastruktur som kode) |
| Overvåking | Application Insights + Log Analytics |
Prosjektstruktur
choir-tickets/
├── api/ # Azure Functions backend
│ ├── src/
│ │ ├── functions/ # Funksjonsendepunkter
│ │ │ ├── auth.ts # Autentisering (magic link)
│ │ │ ├── choirs.ts # Organisasjon-CRUD (superadmin)
│ │ │ ├── users.ts # Brukeradministrasjon (superadmin)
│ │ │ ├── events.ts # Arrangement CRUD + forestillinger
│ │ │ ├── orders.ts # Billettbestillingsflyt
│ │ │ ├── allocations.ts # Billettildelinger per medlem
│ │ │ ├── registrations.ts# Eldre registreringer + historikk
│ │ │ ├── external-sales.ts # Ticketmaster/eksternt salg
│ │ │ ├── reports.ts # Rapportendepunkter
│ │ │ ├── impersonation.ts# Brukerimitasjon med revisjonsspor
│ │ │ └── admin-dashboard.ts # Oversikt + eksport
│ │ └── shared/ # Delte verktøy
│ │ ├── auth.ts # Autentiseringshjelpere + imitasjon
│ │ ├── database.ts # Cosmos DB-klient
│ │ ├── email.ts # E-postsending (Resend)
│ │ └── types.ts # TypeScript-typer
│ ├── scripts/
│ │ ├── seed-admin.ts # Opprett første superadmin
│ │ └── setup-database.ts # Opprett containere med TTL
│ ├── tests/ # Vitest API-tester
│ ├── host.json
│ ├── local.settings.json
│ └── package.json
│
├── frontend/ # Vue 3 PWA
│ ├── src/
│ │ ├── assets/ # CSS, bilder
│ │ ├── components/ # Vue-komponenter
│ │ │ ├── Accordion.vue # Sammenleggbare seksjoner
│ │ │ ├── AccordionItem.vue # Individuell sammenleggbar seksjon
│ │ │ ├── AppLayout.vue # Hovednavigasjonslayout
│ │ │ ├── BottomSheet.vue # Mobilmeny med dra-for-å-lukke
│ │ │ ├── OrganizationSelector.vue # Flerorganisasjonsmeny
│ │ │ ├── ConfirmDialog.vue # Bekreftelsesdialog
│ │ │ ├── DataTable.vue # Responsiv tabell med kortvisning
│ │ │ ├── ErrorBoundary.vue # Feilhåndteringsgrense
│ │ │ ├── ImpersonationBanner.vue # Imitasjonsindikator
│ │ │ ├── Tabs.vue # Fanebeholder (lat, ARIA, tastatur)
│ │ │ └── ToastContainer.vue # Varslingsmeldinger
│ │ ├── composables/ # Vue composables
│ │ │ ├── useApi.ts # API-klient
│ │ │ └── useTheme.ts # Temahåndtering
│ │ ├── i18n/ # Internasjonalisering
│ │ │ ├── en.ts # Engelske oversettelser
│ │ │ ├── no.ts # Norske oversettelser
│ │ │ └── index.ts # i18n-oppsett
│ │ ├── stores/ # Pinia-tilstandslager
│ │ │ └── auth.ts # Autentiseringstilstand + imitasjon
│ │ ├── views/ # Sidekomponenter
│ │ │ ├── admin/ # Admin/ansvarlig-sider
│ │ │ │ ├── DashboardView.vue
│ │ │ │ ├── EventsView.vue
│ │ │ │ ├── EventFormView.vue
│ │ │ │ ├── MembersView.vue
│ │ │ │ ├── MemberSalesView.vue # Individuelt medlemssalg
│ │ │ │ ├── OrdersView.vue # Bestillingsadministrasjon
│ │ │ │ ├── AllocationsView.vue # Tildelingsadministrasjon
│ │ │ │ ├── TicketmasterView.vue # Eksternt salg
│ │ │ │ └── ReportsView.vue
│ │ │ ├── super-admin/ # Superadmin-sider
│ │ │ │ ├── OrganizationsView.vue
│ │ │ │ ├── OrganizationDetailView.vue
│ │ │ │ └── UsersView.vue
│ │ │ ├── HomeView.vue
│ │ │ ├── OrderTicketsView.vue # Medlems billettbestilling
│ │ │ ├── HistoryView.vue
│ │ │ ├── JoinView.vue # Godta invitasjon
│ │ │ ├── LoginView.vue
│ │ │ └── VerifyView.vue
│ │ ├── router/ # Vue Router
│ │ ├── App.vue
│ │ └── main.ts
│ ├── e2e/ # Playwright e2e-tester
│ ├── index.html
│ ├── vite.config.ts
│ └── package.json
│
├── infra/ # Infrastruktur som kode
│ ├── main.bicep # Hoved-Bicep-mal
│ └── main.bicepparam # Parametere
│
├── staticwebapp.config.json # SWA-konfigurasjon
└── README.mdDelt komponentbibliotek
Frontend bruker et egendefinert delt komponentbibliotek bygget med ren Tailwind CSS (ingen eksternt komponentbibliotek). Mørk modus støttes via class-strategien. Alle komponenter ligger i frontend/src/components/.
| Komponent | Beskrivelse |
|---|---|
| Tabs.vue | Gjenbrukbar fanebeholder med lat slot-rendering, ARIA-roller (tablist, tab, tabpanel), tastaturnavigasjon (piltaster) og valgfrie merketall per fane |
| Accordion.vue | Sammenleggbare seksjoner med jevne CSS-overganger. Brukes sammen med AccordionItem.vue |
| AccordionItem.vue | Individuell sammenleggbar seksjon med tittel, feilmerke for valideringstilbakemelding og animert utviding/sammenlegging |
| DataTable.vue | Responsiv tabell med innebygd søk, sortering, paginering og konfigurerbare kolonner. Bytter til kortvisning på mobil |
| BottomSheet.vue | Mobilvennlig modal med dra-for-å-lukke-gest. Brukes til skjemaer og detaljvisninger på små skjermer |
| ConfirmDialog.vue | Gjenbrukbar bekreftelsesdialog som erstatter native confirm() |
| ToastContainer.vue | Varslingssystem som erstatter native alert() |
| OrganizationSelector.vue | Organisasjonsvelger for brukere som tilhører flere organisasjoner |
| ErrorBoundary.vue | Feilgrense som fanger og viser komponentnivåfeil på en pen måte |
| ImpersonationBanner.vue | Rødt banner som indikerer aktiv brukerimitasjonssesjon |
Designmønster: Komponenter bruker rene Tailwind-verktøyklasser uten tredjeparts UI-rammeverk. Mørk modus er implementert via Tailwind class-strategien (dark:-varianter), styrt fra dokumentroten.
Prosjektstyring og orkestrering
Repositoryet inneholder to rammeverk brukt for prosjektstyring og koordinering:
.specify/— Rammeverk for prosjektstyring som definerer arkitekturprinsipper (konstitusjon) og maler for funksjonsspesifikasjoner. Spesifikasjoner ligger ispecs/og følger malene definert her..squad/— System for multi-agent-orkestrering med spesialiserte agenter for frontend, backend, infrastruktur, QA og dokumentasjon. Definerer roller, arbeidsflyter og sprintkoordinering.
Disse er valgfrie for bidragsytere, men refereres i PR-gjennomgangssjekklister og bidrar til konsistens på tvers av prosjektet.
Nøkkelkonsepter
To billettarbeidsflyter
Appen støtter to ulike arbeidsflyter for forskjellige brukstilfeller:
Bestillingsflyt (medlemsinitialisert, godkjenning påkrevd):
- Medlemmer bestiller billetter → Billettansvarlig godkjenner → Markerer som levert
- Statusflyt: VENTENDE → GODKJENT → LEVERT (eller KANSELLERT)
- Brukstilfelle: Rettferdig fordeling med oversikt
- Funksjoner: Bestillingshistorikk, admin-notater, overføring mellom forestillinger
Registreringsflyt (admin-initiert, direktesalg):
- Admin/billettansvarlig oppretter registrering direkte (ingen godkjenning nødvendig)
- 1 times frist for redigering/sletting
- Brukstilfelle: Salg over disk, billettluke-transaksjoner
- Enklere og raskere for salg på stedet
Når bør hvilken arbeidsflyt brukes?
| Scenario | Anbefalt arbeidsflyt |
|---|---|
| Medlemmer bestiller billetter for seg selv | Bestillinger (godkjenning påkrevd) |
| Salg over disk ved arrangement | Registreringer (direktesalg) |
| Billettluke-transaksjoner | Registreringer (direktesalg) |
| Forhåndstildeling av billetter | Bestillinger (med tildelinger) |
| VIP/sponsor gratisbilletter | Fribilletter |
| Eksternt plattformsalg (Ticketmaster) | Eksternt salg-sporing |
Kapasitetssporing
Systemet sporer salskapasitet på flere dimensjoner:
- Medlemssalg: Bestillinger (godkjent/levert) + Registreringer
- Eksternt salg: Billetter solgt via Ticketmaster, billettluke, osv.
- Fribilletter: Gratisbilletter for VIP-er, sponsorer
- Totalt solgt: Medlemssalg + Eksternt salg + Fribilletter
- Gjenstående: Salskapasitet − Totalt solgt
Alt spores per forestilling med sanntidsoppdateringer.
Invitasjonssystem
Utvid organisasjonsmedlemskapet gjennom delbare invitasjonskoder:
Admin oppretter invitasjon med valgfri:
- Bruksgrense (f.eks. 50 bruk)
- Utløpsdato
- Standardrolle for nye medlemmer
Del lenke:
https://din-app.com/join/{kode}Nye medlemmer:
- Klikk lenke → Oppgi detaljer → Godta invitasjon
- Konto opprettes automatisk og kobles til organisasjonen
- Innloggings-e-post sendes automatisk
Admin kan tilbakekalle invitasjoner når som helst
Beskyttelsesperiode
For å forhindre utilsiktet datatap og samtidig tillate raske korrigeringer:
- Registreringer: 1 times frist for redigering/sletting
- Etter fristen: Låst for å beskytte historiske data
- Bestillinger: Ingen frist (spores gjennom statusendringer)
Dette sikrer at økonomiske data forblir nøyaktige, samtidig som det gir fleksibilitet for feil.
Applikasjonsflyter
Autentiseringsflyt
Magic Link-flyt:
- Bruker skriver inn e-post på
/login - Systemet sender e-post med magic link + 6-sifret verifiseringskode
- Bruker kan enten:
- Klikke magic link → Automatisk verifisering → Omdirigeres til appen
- Skrive inn 6-sifret kode manuelt → Verifisering → Omdirigeres til appen
- JWT-sesjonstoken utstedes (lagres i localStorage)
- Token utløper basert på
JWT_EXPIRY-innstilling
- Bruker skriver inn e-post på
Sikkerhetsfunksjoner:
- Magic-lenker utløper etter 30 minutter
- Verifiseringskoder utløper etter 30 minutter
- Lenker og koder kan brukes én gang
- Brukeroppslaget fullføres før legitimasjonen brukes, og en ETag-beskyttet skriving hindrer samtidig dobbeltbruk
- Hastighetsbegrensning: 5 forsøk per IP/e-post per 15 minutter
- Generiske svar (ingen e-postenumerering)
Medlemsregistreringsflyt
Via invitasjonskode:
- Admin oppretter invitasjonskode med valgfri bruksgrense og utløpsdato
- Del invitasjonslenke:
https://app.com/join/{kode} - Ny bruker klikker lenke → Skriver inn e-post og navn → Godtar invitasjon
- Konto opprettes og kobles til organisasjon med angitt rolle
- Innloggings-e-post sendes automatisk
Via admin-import:
- Admin laster opp CSV med e-post, navn, rolle
- Kontoer opprettes i bulk
- Valgfritt: Invitasjons-e-poster sendes til nye medlemmer
Billettbestillingsflyt (godkjenningsarbeidsflyt)
Medlem oppretter bestilling:
- Bla gjennom aktive arrangementer på hjemmesiden
- Velg arrangement og forestilling
- Velg billetttyper og antall
- Legg til valgfritt personlig notat
- Fortsett til sammendraget og legg valget i handlekurven
- Se en vedvarende bekreftelse, og åpne deretter handlekurven eller legg til flere billetter
- Send inn handlekurven (status: VENTENDE, én bestilling per arrangement)
Billettansvarlig godkjenner:
- Se den eldste ventende bestillingen først, med lokal dato og klokkeslett for innsending
- Filtrer aktive arbeidskøer etter arrangement og deler av medlemsnavnet
- Gjennomgå bestillingsdetaljer og medlemshistorikk
- Godkjenn eller avvis med valgfritt admin-notat
- Status endres til GODKJENT eller KANSELLERT
Levering:
- Billettansvarlig markerer bestilling som LEVERT når billetter gis til medlem
- Medlem ser LEVERT-status i sin historikk
Spesialtilfeller:
- Medlemmer kan kansellere ventende bestillinger. Handlinger for ansvarlige følger livssyklusreglene
- Godkjente/leverte bestillinger kan overføres mellom forestillinger
- Arkivering er et presentasjonsflagg bare for administratorer og endrer ikke livssyklusstatus, avstemmingsdata eller handlinger
- Se direktesalgsflyt nedenfor for registreringsspesifikk oppførsel
Direktesalgsflyt (registreringer)
Admin/billettansvarlig:
- Naviger til arrangementsoversikt
- Opprett registrering direkte (hopper over godkjenning)
- Velg medlem, forestilling, billetttyper
- Registrering opprettes umiddelbart
Frist:
- 1 times vindu for å redigere eller slette registrering
- Etter 1 time er registreringen låst (forhindrer utilsiktet sletting)
Eksternt salg-sporing
Billettansvarlig registrerer eksternt salg:
- Naviger til Ticketmaster/eksternt salg-visning
- Velg arrangement og forestilling
- Skriv inn kilde (f.eks. «Ticketmaster», «Billettluke»)
- Legg til billetttyper og antall
- Valgfritt: Legg til billettgebyr
Kapasitetssporing:
- Oversikten viser: Medlemssalg + Eksternt salg + Fribilletter = Totalt solgt
- Gjenstående = Salskapasitet − Totalt solgt
Tildelingsadministrasjon
Admin setter tildelinger:
- Naviger til tildelingsvisning
- Velg medlem, arrangement, forestilling
- Sett billettgrenser per billetttype
- Medlem ser tildelingsgrenser ved bestilling
Medlem ser tildelinger:
- Personlig oversikt viser tildelte billetter
- Tildelingsgrenser vises under bestilling
Fribillettadministrasjon
Admin oppretter fribilletter:
- Naviger til arrangementadministrasjon
- Legg til gratisbilletter for VIP-er, sponsorer osv.
- Velg forestilling og billetttyper
- Fribilletter teller mot kapasiteten
Kapasitetsbevisst:
- Fribilletter reduserer tilgjengelig kapasitet
- Oversikten viser fordeling av fribilletter
Imitasjonsflyt (superadmin)
Superadmin imiterer bruker:
- Naviger til brukervisning
- Klikk «Imiter» på målbrukeren
- Systemet logger: adminUserId, targetUserId, tidsstempel, action: 'start'
- Rødt banner vises: «Viser som [Brukernavn]»
- Alle handlinger logges til imitasjonsrevisjonssporet
Avslutt imitasjon:
- Klikk «Slutt å imitere» i banneret
- Systemet logger: action: 'stop', varighet
- Tilbake til superadmin-visning
Revisjonsspor:
- Alle imitasjonsøkter logges med varighet
- Admins kan gjennomgå imitasjonshistorikk
Flerorganisasjonsadministrasjon
Superadmin oppretter organisasjon:
- Naviger til organisasjonsvisning → Opprett organisasjon
- Skriv inn organisasjonsnavn og detaljer
- Systemet oppretter organisasjonsentitet
Tildel medlemmer til organisasjon:
- Importer via CSV eller legg til individuelt
- Sett rolle per medlem (medlem, billettansvarlig, kasserer, admin)
Medlem i flere organisasjoner:
- Organisasjonsvelger vises i navigasjonen
- Bytt mellom organisasjoner
- All data (arrangementer, bestillinger, rapporter) er scopet til valgt organisasjon
Rapportering og eksport
Se rapporter:
- Naviger til rapportvisning (kasserer+)
- Velg arrangement og datoperiode
- Filtrer etter spesifikt medlem med nedtrekksmenyen
- Se økonomisk oppsummering og fordeling
Eksporter data:
- Oversikt → Eksporter Excel (flerspråksstøtte)
- Rapporter → Eksporter CSV
- Data inkluderer: medlemssalg, eksternt salg, fribilletter, inntekt
Kapasitetsmodell
venueCapacityer grensen for hver forestilling.- Solgt antall beregnes per forestilling fra rapporterte bestillingssalg, direkteregistreringer, eksternt salg og fribilletter.
- Gjenstående per forestilling =
max(0, venueCapacity − solgt antall). - Samlet kapasitet =
venueCapacity × antall forestillinger. - Samlet gjenstående er summen av gjenstående plasser for hver forestilling. En oversolgt forestilling låner ikke kapasitet fra en annen forestilling.
Brukerroller
| Rolle | Tillatelser |
|---|---|
member | Bestille billetter, se egen historikk og tildelinger |
ticketManager | Alle medlemstillatelser + administrere bestillinger, tildelinger, eksternt salg |
treasurer | Alle billettansvarlig-tillatelser + tilgang til rapporter og eksport |
admin | Alle kasserer-tillatelser + administrere arrangementer, medlemmer, oversikt |
super admin | Alle admin-tillatelser + opprette/administrere organisasjoner, administrere alle brukere, imitere brukere |
Rollematrise
| Funksjon | Medlem | Billettansvarlig | Kasserer | Admin | Superadmin |
|---|---|---|---|---|---|
| Bestille billetter | ✅ | ✅ | ✅ | ✅ | ✅ |
| Se egen historikk | ✅ | ✅ | ✅ | ✅ | ✅ |
| Administrere bestill. | ❌ | ✅ | ✅ | ✅ | ✅ |
| Administrere tildelinger | ❌ | ✅ | ✅ | ✅ | ✅ |
| Registrere eksternt salg | ❌ | ✅ | ✅ | ✅ | ✅ |
| Se rapporter | ❌ | ❌ | ✅ | ✅ | ✅ |
| Eksportere rapporter | ❌ | ❌ | ✅ | ✅ | ✅ |
| Se oversikt | ❌ | ❌ | ❌ | ✅ | ✅ |
| Administrere arrangement | ❌ | ❌ | ❌ | ✅ | ✅ |
| Administrere medlemmer | ❌ | ❌ | ❌ | ✅ | ✅ |
| Administrere alle organisasjoner | ❌ | ❌ | ❌ | ❌ | ✅ |
| Imitere brukere | ❌ | ❌ | ❌ | ❌ | ✅ |
Navigasjonsstruktur
| Seksjon | Rolle påkrevd | Sider (rollefiltrert) |
|---|---|---|
| Medlem | Alle brukere | Hjem, Bestill billetter, Min historikk |
| Administrasjon | ticketManager+ | Bestillinger, Forpliktelser, Ticketmaster, Tildelinger, Rapporter, Oversikt, Arrangementer, Medlemmer |
| Superadmin | super admin | Organisasjoner, Alle brukere |
Merk: Administrasjon-seksjonen samler de tidligere Billettansvarlig-, Kasserer- og Admin-seksjonene. Elementer er rollefiltrert — hver bruker ser kun sidene rollen tillater (f.eks. en billettansvarlig ser Bestillinger, Forpliktelser, Ticketmaster og Tildelinger, men ikke Rapporter eller Oversikt).
Sikkerhet
Autentiseringssikkerhet
- Magic Link + verifiseringskode — Ingen passord lagres; innlogging via e-postlenke eller 6-sifret kode
- Dobbel autentiseringsmetode — Brukere kan klikke magic link i e-posten ELLER skrive inn verifiseringskoden for enklere mobiltilgang
- Generiske svar — Innloggingsflyten returnerer samme melding uansett om e-posten finnes eller ikke (forhindrer e-postenumerering)
- Tokenutløp: Magic-lenker og verifiseringskoder utløper etter 30 minutter
- JWT-sesjoner — Tilstandsløse sesjonstokener med konfigurerbar utløpstid
Hastighetsbegrensning
Hastighetsbegrensning er implementert med Cosmos DB med TTL for automatisk opprydding:
| Grensetype | Terskel | Vindu | Oppførsel |
|---|---|---|---|
| Per IP | 5 | 15 minutter | HTTP 429 med Retry-After |
| Per e-post | 5 | 15 minutter | Stille (returnerer generisk suksess) |
Hvorfor Cosmos DB for hastighetsbegrensning?
- Azure Static Web Apps / Functions er serverløse — hastighetsbegrensning i minnet fungerer ikke på tvers av instanser
- TTL sletter automatisk utløpte oppføringer (ingen RU-kostnad for opprydding)
- Kostnad: ~2–3 RU-er per hastighetsbegrenset forespørsel
- Hastighetsbegrensede spam-forespørsler spør ikke users-containeren — sparer RU-er
Delte arrangementer
Delt-arrangement-modulen legger til et koordineringslag over enkeltorganisasjonens arrangementsmodell. Modulen er strengt additiv: alle eksisterende entiteter, containere, salgsflyter, rapporter og oversikter er uendret når ingen delte arrangementer er involvert.
Design: koordinator pluss projeksjoner
Arkitekturen bruker en koordinator-pluss-projeksjoner-modell med en sentralisert kapasitetsregnskapstjeneste:
┌──────────────────────────────────────────────────────────────┐
│ SharedEvent (koordinator) │
│ Én per konsert — lagrer identitet, kapasitetspolicy, │
│ deltakerliste, tillatelser, revisjonslogg og kanaltildelinger│
└──────────┬─────────────────────────────────┬─────────────────┘
│ │
┌─────▼──────┐ ┌──────▼─────┐
│ Event │ │ Event │ (per-org lokale projeksjoner)
│ (org A) │ │ (org B) │
│ ← eksist. │ │ ← eksist. │
│ pipeline │ │ pipeline │
└─────┬──────┘ └──────┬─────┘
│ │
└─────────────────┬───────────────┘
│
┌───────────────────▼──────────────────────┐
│ sharedEventCapacityLedger │
│ Sentraliserte kapasitetstellere og │
│ reservasjoner — håndheves per │
│ forestilling i både kvote- og pool-modus │
│ via transaksjonelle batch-operasjoner │
└──────────────────────────────────────────┘To nye Cosmos DB-containere
| Container | Formål |
|---|---|
sharedEvents (partisjon /sharedEventId) | Koordinatorrot, deltakerliste, revisjonslogg, projeksjons-synkroniseringsjobber, kapasitetsvakter, kapasitetssperring |
sharedEventCapacityLedger (partisjon /sharedEventId, defaultTtl -1) | Kapasitetstellere, reservasjoner (med utløp-TTL), justeringer, kapasitetssperrdokument |
Totalt antall containere etter disse to nye er 21 av 25 (4 i reserve innenfor gratistier-grensen).
Kapasitetstjenesten
Alle kapasitetspåvirkende operasjoner i både kvote- og pool-modus går gjennom den sentraliserte shared-event-capacity.ts-tjenesten. Den bruker Cosmos PatchOperation.incr() med betingelsesfiltre på serversiden inne i transaksjonelle batch-operasjoner — ikke ETag lese-endre-skrive — for atomisitet under simultanbelastning.
Reservasjonsflyt (pool-modus):
reservasjonsbatch (atomisk):
ops[0] — CapacityFenceDoc: Create ifNoneMatch:* (init) eller Replace ifMatch:_etag (valider tilstand)
ops[1] — CapacityCounter: patch-incr reserved med betingelse reserved+committed+qty <= capacity
ops[2] — CapacityReservation: Create ifNoneMatch:* (idempotensivakt)Bekreft/frigjør-flyt:
- Bekreft: patch-incr
−reserved,+committed; patch reservasjonheld → committed. - Frigjør: patch-incr
−reserved; patch reservasjonheld → releasedmed terminal TTL.
Tellernøkkelskjema:
- Pool-modus:
pool:{showtimeId}— én felles teller per forestilling. - Kvotemodus:
quota:{organizationId}:{showtimeId}— én teller per org per forestilling.
Avlysings-saga
Avlysings-sagaen overgår open|locked → cancelling → cancelled med garantert linearisering via det permanente CapacityFenceDoc:
acquireCancellationFence— sperren overføres til"cancelling"(lineariseringspunkt; samtidige reservasjonsbatcher mislykkes i sperre-sjekken).- Rotdokument overføres til
cancelling(ETag-voktet;revertCancellationFencegjenoppretter en mislykket CAS). - Kapasitetsvakt anskaffet (TTL-begrenset eksklusiv skrivelås).
- Deltaker-projeksjoner låses.
- Alle holdte reservasjoner tømmes (page fra
null-fortsettelse hver gang for å unngå mutasjonshopp). - Sperren overføres til
"cancelled"(permanent; utløper aldri). - Rotdokument overføres til
cancelled+ revisjonspost.
Avlysing refunderer ikke bekreftede salg. Bekreftede salg bevares nøyaktig slik de er registrert. Applikasjonen har ingen kundeparablangrefusjons-arbeidsflyt i v1. Se guide/shared-event-organizer.md for ingen-refusjon-semantikken.
Kapasitetspolicy-overganger
Overganger mellom kvotemodus og pool-modus bruker en fryse-og-anvend-vakt (CapacityGuardDoc med TTL) som serialiserer overgangen med samtidige kjøp:
- Mens vakten holdes, returnerer
reserveCapacity-kall en prøvbar 503. - Vakten utløper automatisk ved proseskrasj, og hindrer permanent låsing.
- To samtidige overganger kan ikke begge gjennomføres (ETag-konflikt).
Deterministiske projeksjons-IDer
Når en deltaker aksepterer, oppretter projeksjons-synkroniseringsjobben et lokalt Event-dokument med en deterministisk ID: Event.id == sharedEventId (org-partisjonert). Dette sikrer at avbrutte aksept-sagaer er idempotente ved gjenforsøk.
Absolutt rekonsiliering
Kapasitetstjenesten kan rekonsilieres den felles pool-telleren mot autoritative per-organisasjons salgsregistreringer via runAbsoluteReconciliation. Rekonsiliering favoriserer alltid per-org salgsregistreringer (sannhetskilden for penger tatt inn) og korrigerer registeret mot dem.
Eksterne salgskanaler
Kanaltildelinger lagres på SharedEvent-roten. connectionRef-feltet, der det finnes, er en Azure Key Vault-hemmelighets-URI-referanse — rå legitimasjon lagres aldri i Cosmos. Kanaltildelingsendringer protokollføres.
Billetto-runtime er utsatt til #242. OAuth-legitimasjonsløser, webhook-mottak, deltaker-/refusjons-/avlysingssynkronisering og leverandørens runtime-operasjoner er ikke implementert ennå. Manuelle Ticketmaster-oppføringer er fullt operative.
Nye API-ruter (modul for delte arrangementer)
Modulen for delte arrangementer legger til følgende Azure Function-ruter under api/src/functions/:
| Modul | Ruter |
|---|---|
shared-events.ts | GET/POST /api/shared-events; GET/PUT/DELETE /api/shared-events/{id}; listehåndtering, kapasitetspolicy, overganger, rapportering; organisasjonssøk (kun superadmin) |
shared-events.ts | POST /api/shared-events/{id}/invitation-links — generer lenke (eier eller manageParticipants; hastighetsbegrenset) |
shared-events.ts | GET /api/shared-events/{id}/invitation-links — list lenker; returnerer kun metadata, aldri token eller hash |
shared-events.ts | DELETE /api/shared-events/{id}/invitation-links/{linkId} — tilbakekall lenke (eier eller manageParticipants) |
shared-events.ts | POST /api/shared-event-invitations/accept — aksepter lenke (autentisert admin; token i forespørselskroppen; hastighetsbegrenset) |
shared-events.ts | GET /api/shared-event-invitations/preview — forhåndsvis arrangement og arrangørnavn for en lenke (?token=...) |
shared-events.ts | GET /api/shared-event-invitations/pending — list aktive invitasjoner for den autentiserte brukerens admin-orger (ingen token kreves; D16) |
shared-events.ts | POST /api/shared-event-invitations/pending/accept — aksepter via inn-app-stien med invitasjonsdokument-ID (tokenfri; samme ETag-linearisering; D16) |
shared-event-channels.ts | GET/PUT/DELETE /api/shared-events/{id}/external-sales-channels/{source} |
shared-event-maintenance.ts | POST /api/management/shared-event-maintenance (superAdmin) — full vedlikeholdspipeline; opportunistisk utløp fra kapasitetsmutasjoner |
Invitasjonslenker
Invitasjonslenker gir en samtykkesbasert måte å legge til organisasjoner i et delt arrangement på, uten at arrangøren trenger å søke i plattformens fullstendige organisasjonsregister. Organisasjonssøk er forbeholdt plattform-superadmins.
- Token-generering:
crypto.randomBytes(32)→ base64url-råtoken (43 tegn). Dokumentet lagrer kun SHA-256-hashen; råtoken returneres én gang og beholdes aldri. - Lagring: invitasjonslenke-dokumenter lagres i
sharedEvents-containeren under samme partisjonsnøkkel (/sharedEventId) som arrangementsroten. Ingen ny container er nødvendig. - Hastighetsbegrensning: generering og aksept er hastighetsbegrenset per
invitation-handlingsklasse (10 operasjoner per 15-minutters vindu per delt arrangement). - Aksept-atomisitet: ETag-betinget erstatning merker invitasjonen
usedog legger organisasjonen til på listen. Dersom to samtidige akseptanter forsøker, vinner nøyaktig én; den andre mottar en generisk 404. - Aksept-gjenoppretting:
usedByOrganizationIdpå invitasjonsdokumentet er et varig gjenopprettingsmerke. Dersom aksept-sagaen avbrytes etter merking avusedmen før listeoppdatering, oppdager et gjenforsøk merket og fullfører listetrinnet idempotent. - Generiske svar: alle feilstier (utløpt, brukt, tilbakekalt, ugyldig token, feil arrangementstatus) returnerer samme
404 { success: false, error: "Invitation link is not available" }med 50–200 ms tilfeldig forsinkelse for å hindre tidsbasert opptelling. - Ingen e-postlagring: dersom arrangøren oppgir en e-postadresse, forsøker plattformen levering og registrerer kun
emailDeliveryAttempted: booleanpå invitasjonsdokumentet. E-postadressen lagres eller logges aldri. - Revisjonsredigering: token-hash, råtoken og e-postadresse inkluderes aldri i revisjonslogger; kun dokument-IDen (avledet fra hash-prefikset) registreres.
Varsler om invitasjoner i appen (D16)
Når en invitasjonslenke for delt arrangement opprettes med en valgfri e-postadresse, utfører systemet et best-effort asynkront bieffekt-oppslag for å sjekke om e-posten tilhører en kjent, godkjent organisasjonsadmin (via isKnownOrgAdmin). Hvis en match finnes, spres én varsling i appen til hver organisasjonspartisjon den matchede brukeren administrerer.
Viktige designpunkter:
- Fire-and-forget: fan-uten startes etter at invitasjonslenke-opprettelsessvaret er ferdigstilt. Enhver feil (Cosmos-feil, tidsavbrudd, delvis fan-ut) påvirker ikke invitasjonslenken eller svaret til arrangøren.
- Anti-oracle: arrangøren mottar et identisk svar uavhengig av om en match ble funnet, og om en varsling ble opprettet, lest eller reagert på.
- Tokenfri handlings-URL: varslingens
actionUrler/shared-events/pending-invitations— en sesjonsgodkjent side som henter live invitasjonsstatus. Ingen invitasjonstoken eller hash er innebygd i varslingsdokumentet. - Personvern: varslingsdokumentet inneholder aldri mottakerens e-postadresse, råtoken, token-hash eller invitasjonsdokument-ID. Kun avgrensede systemidentifikatorer lagres:
sharedEventId,sharedEventName,organizerOrganizationName. - Dedup: hver varsling bruker en deterministisk dokument-ID avledet fra
{invitationDocId}:{recipientUserId}:{orgId}. Samtidige eller gjentatte fan-ut-forsøk for den samme tripelen er stille idempotente (Cosmos 409 = suksess). - Ingen nye containere: varsler bruker den eksisterende
notifications-containeren partisjonert etter/organizationId. - TTL: begrenset av invitasjonens gjenværende levetid (opptil 15 dager). Utdaterte varsler (invitasjon brukt, tilbakekalt eller utløpt) fører til en tom side for ventende invitasjoner — ikke en feil.
- Mottakerbinding:
GET /api/shared-event-invitations/pending-endepunktet henter invitasjoner derrecipientUserIdsamsvarer med den autentiserte sesjonbrukeren.POST .../accept-endepunktet verifiserer samme binding før domenesjekker, og returnerer det generiske «lenken er ikke tilgjengelig»-svaret ved avvik.
Bakoverkompatibilitet
Modulen rulles ut bak en funksjonsflagg. Med flagget av er alle eksisterende arrangement-, bestillings-, registrerings-, tildeling-, fribilett-, eksterntsalg-, rapport- og oversiktsflyter helt uendret. Et frittstående arrangement (uten sharedEventLink) oppfører seg nøyaktig som før.
Neste: Datamodell · Se også: API-referanse · Miljøvariabler