Skip to content

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

LagTeknologi
FrontendVue 3 + Vite + Tailwind CSS + PWA + vue-i18n
BackendAzure Functions (Node.js / TypeScript)
DatabaseAzure Cosmos DB (Gratis tier)
HostingAzure Static Web Apps (Gratis tier)
InfrastrukturBicep (Infrastruktur som kode)
OvervåkingApplication 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.md

Delt 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/.

KomponentBeskrivelse
Tabs.vueGjenbrukbar fanebeholder med lat slot-rendering, ARIA-roller (tablist, tab, tabpanel), tastaturnavigasjon (piltaster) og valgfrie merketall per fane
Accordion.vueSammenleggbare seksjoner med jevne CSS-overganger. Brukes sammen med AccordionItem.vue
AccordionItem.vueIndividuell sammenleggbar seksjon med tittel, feilmerke for valideringstilbakemelding og animert utviding/sammenlegging
DataTable.vueResponsiv tabell med innebygd søk, sortering, paginering og konfigurerbare kolonner. Bytter til kortvisning på mobil
BottomSheet.vueMobilvennlig modal med dra-for-å-lukke-gest. Brukes til skjemaer og detaljvisninger på små skjermer
ConfirmDialog.vueGjenbrukbar bekreftelsesdialog som erstatter native confirm()
ToastContainer.vueVarslingssystem som erstatter native alert()
OrganizationSelector.vueOrganisasjonsvelger for brukere som tilhører flere organisasjoner
ErrorBoundary.vueFeilgrense som fanger og viser komponentnivåfeil på en pen måte
ImpersonationBanner.vueRø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 i specs/ 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:

  1. 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
  2. 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?

ScenarioAnbefalt arbeidsflyt
Medlemmer bestiller billetter for seg selvBestillinger (godkjenning påkrevd)
Salg over disk ved arrangementRegistreringer (direktesalg)
Billettluke-transaksjonerRegistreringer (direktesalg)
Forhåndstildeling av billetterBestillinger (med tildelinger)
VIP/sponsor gratisbilletterFribilletter
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:

  1. Admin oppretter invitasjon med valgfri:

    • Bruksgrense (f.eks. 50 bruk)
    • Utløpsdato
    • Standardrolle for nye medlemmer
  2. Del lenke: https://din-app.com/join/{kode}

  3. Nye medlemmer:

    • Klikk lenke → Oppgi detaljer → Godta invitasjon
    • Konto opprettes automatisk og kobles til organisasjonen
    • Innloggings-e-post sendes automatisk
  4. 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

  1. 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
  2. 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

  1. 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
  2. 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)

  1. 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)
  2. 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
  3. Levering:

    • Billettansvarlig markerer bestilling som LEVERT når billetter gis til medlem
    • Medlem ser LEVERT-status i sin historikk
  4. 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)

  1. Admin/billettansvarlig:

    • Naviger til arrangementsoversikt
    • Opprett registrering direkte (hopper over godkjenning)
    • Velg medlem, forestilling, billetttyper
    • Registrering opprettes umiddelbart
  2. Frist:

    • 1 times vindu for å redigere eller slette registrering
    • Etter 1 time er registreringen låst (forhindrer utilsiktet sletting)

Eksternt salg-sporing

  1. 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
  2. Kapasitetssporing:

    • Oversikten viser: Medlemssalg + Eksternt salg + Fribilletter = Totalt solgt
    • Gjenstående = Salskapasitet − Totalt solgt

Tildelingsadministrasjon

  1. Admin setter tildelinger:

    • Naviger til tildelingsvisning
    • Velg medlem, arrangement, forestilling
    • Sett billettgrenser per billetttype
    • Medlem ser tildelingsgrenser ved bestilling
  2. Medlem ser tildelinger:

    • Personlig oversikt viser tildelte billetter
    • Tildelingsgrenser vises under bestilling

Fribillettadministrasjon

  1. Admin oppretter fribilletter:

    • Naviger til arrangementadministrasjon
    • Legg til gratisbilletter for VIP-er, sponsorer osv.
    • Velg forestilling og billetttyper
    • Fribilletter teller mot kapasiteten
  2. Kapasitetsbevisst:

    • Fribilletter reduserer tilgjengelig kapasitet
    • Oversikten viser fordeling av fribilletter

Imitasjonsflyt (superadmin)

  1. 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
  2. Avslutt imitasjon:

    • Klikk «Slutt å imitere» i banneret
    • Systemet logger: action: 'stop', varighet
    • Tilbake til superadmin-visning
  3. Revisjonsspor:

    • Alle imitasjonsøkter logges med varighet
    • Admins kan gjennomgå imitasjonshistorikk

Flerorganisasjonsadministrasjon

  1. Superadmin oppretter organisasjon:

    • Naviger til organisasjonsvisning → Opprett organisasjon
    • Skriv inn organisasjonsnavn og detaljer
    • Systemet oppretter organisasjonsentitet
  2. Tildel medlemmer til organisasjon:

    • Importer via CSV eller legg til individuelt
    • Sett rolle per medlem (medlem, billettansvarlig, kasserer, admin)
  3. Medlem i flere organisasjoner:

    • Organisasjonsvelger vises i navigasjonen
    • Bytt mellom organisasjoner
    • All data (arrangementer, bestillinger, rapporter) er scopet til valgt organisasjon

Rapportering og eksport

  1. Se rapporter:

    • Naviger til rapportvisning (kasserer+)
    • Velg arrangement og datoperiode
    • Filtrer etter spesifikt medlem med nedtrekksmenyen
    • Se økonomisk oppsummering og fordeling
  2. Eksporter data:

    • Oversikt → Eksporter Excel (flerspråksstøtte)
    • Rapporter → Eksporter CSV
    • Data inkluderer: medlemssalg, eksternt salg, fribilletter, inntekt

Kapasitetsmodell

  • venueCapacity er 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

RolleTillatelser
memberBestille billetter, se egen historikk og tildelinger
ticketManagerAlle medlemstillatelser + administrere bestillinger, tildelinger, eksternt salg
treasurerAlle billettansvarlig-tillatelser + tilgang til rapporter og eksport
adminAlle kasserer-tillatelser + administrere arrangementer, medlemmer, oversikt
super adminAlle admin-tillatelser + opprette/administrere organisasjoner, administrere alle brukere, imitere brukere

Rollematrise

FunksjonMedlemBillettansvarligKassererAdminSuperadmin
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
SeksjonRolle påkrevdSider (rollefiltrert)
MedlemAlle brukereHjem, Bestill billetter, Min historikk
AdministrasjonticketManager+Bestillinger, Forpliktelser, Ticketmaster, Tildelinger, Rapporter, Oversikt, Arrangementer, Medlemmer
Superadminsuper adminOrganisasjoner, 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:

GrensetypeTerskelVinduOppførsel
Per IP515 minutterHTTP 429 med Retry-After
Per e-post515 minutterStille (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

ContainerFormå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 reservasjon held → committed.
  • Frigjør: patch-incr −reserved; patch reservasjon held → released med 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:

  1. acquireCancellationFence — sperren overføres til "cancelling" (lineariseringspunkt; samtidige reservasjonsbatcher mislykkes i sperre-sjekken).
  2. Rotdokument overføres til cancelling (ETag-voktet; revertCancellationFence gjenoppretter en mislykket CAS).
  3. Kapasitetsvakt anskaffet (TTL-begrenset eksklusiv skrivelås).
  4. Deltaker-projeksjoner låses.
  5. Alle holdte reservasjoner tømmes (page fra null-fortsettelse hver gang for å unngå mutasjonshopp).
  6. Sperren overføres til "cancelled" (permanent; utløper aldri).
  7. 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/:

ModulRuter
shared-events.tsGET/POST /api/shared-events; GET/PUT/DELETE /api/shared-events/{id}; listehåndtering, kapasitetspolicy, overganger, rapportering; organisasjonssøk (kun superadmin)
shared-events.tsPOST /api/shared-events/{id}/invitation-links — generer lenke (eier eller manageParticipants; hastighetsbegrenset)
shared-events.tsGET /api/shared-events/{id}/invitation-links — list lenker; returnerer kun metadata, aldri token eller hash
shared-events.tsDELETE /api/shared-events/{id}/invitation-links/{linkId} — tilbakekall lenke (eier eller manageParticipants)
shared-events.tsPOST /api/shared-event-invitations/accept — aksepter lenke (autentisert admin; token i forespørselskroppen; hastighetsbegrenset)
shared-events.tsGET /api/shared-event-invitations/preview — forhåndsvis arrangement og arrangørnavn for en lenke (?token=...)
shared-events.tsGET /api/shared-event-invitations/pending — list aktive invitasjoner for den autentiserte brukerens admin-orger (ingen token kreves; D16)
shared-events.tsPOST /api/shared-event-invitations/pending/accept — aksepter via inn-app-stien med invitasjonsdokument-ID (tokenfri; samme ETag-linearisering; D16)
shared-event-channels.tsGET/PUT/DELETE /api/shared-events/{id}/external-sales-channels/{source}
shared-event-maintenance.tsPOST /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 used og legger organisasjonen til på listen. Dersom to samtidige akseptanter forsøker, vinner nøyaktig én; den andre mottar en generisk 404.
  • Aksept-gjenoppretting: usedByOrganizationId på invitasjonsdokumentet er et varig gjenopprettingsmerke. Dersom aksept-sagaen avbrytes etter merking av used men 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: boolean på 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 actionUrl er /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 der recipientUserId samsvarer 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

Built with VitePress