openapi: 3.0.3
info:
  title: Guidad Felsökning – plattforms-API
  version: 1.0.0
  description: |
    API-first-ytan för Guidad Felsökning: autentisering, organisationens
    användare, ärenden med append-only händelselogg, arbetsledaröversikt,
    publik Live Share-delning samt AI-orkestern.

    **Händelseloggen är append-only.** Det finns inga update- eller
    delete-operationer — historik kan aldrig ändras eller raderas, vilket
    även garanteras av databastriggers.

    **Händelse-id är skopade till ärendet.** Nyckeln är `(arende_id, id)`,
    inte `id` ensamt. En omsändning av samma händelse med samma innehåll är
    idempotent och svarar `200`. Samma id med ANNAT innehåll svarar `409`
    och skriver ingenting — varken det som redan står i loggen eller resten
    av satsen.

    **Multi-tenant.** All ärendedata är organisationsknuten. Ett ärende i
    en annan organisation ger `404`, som om det inte fanns.

    Specen serveras live av tjänsten på `GET /api/openapi.yaml`.
servers:
  - url: https://app.exempel.se
    description: Klustrets ingress (ersätt med er domän)
tags:
  - name: Auth
  - name: Användare
  - name: Ärenden
  - name: Översikt
  - name: Delning
  - name: AI
  - name: Integrationer
  - name: Drift
  - name: Fakturering

paths:
  /halsa:
    get:
      tags: [Drift]
      summary: Hälsokontroll
      security: []
      responses:
        "200":
          description: Tjänsten svarar.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }

  /api/openapi.yaml:
    get:
      tags: [Drift]
      summary: Denna specifikation
      security: []
      responses:
        "200":
          description: OpenAPI-specen i YAML.
          content:
            application/yaml:
              schema: { type: string }

  /api/auth/registrera:
    post:
      tags: [Auth]
      summary: Skapa organisation + systemadministratör
      description: >
        Registrerar en ny organisation (tenant) och dess första användare,
        som blir systemadministratör. Kan stängas av driften
        (REGISTRERING_OPPEN=false → 403).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [epost, losenord, namn, organisation]
              properties:
                epost: { type: string, format: email }
                losenord: { type: string, minLength: 8 }
                namn: { type: string }
                organisation: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Inloggad" }
        "400": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "409": { $ref: "#/components/responses/Fel" }

  /api/auth/logga-in:
    post:
      tags: [Auth]
      summary: Logga in
      description: >
        Takt-begränsad i databasen, så spärren håller bakom flera
        repliker: 10 misslyckade försök per konto och 30 per källadress
        inom 15 minuter ger 429. Spärren gäller kontot även vid rätt
        lösenord — annars vore den meningslös. Ett avstängt konto svarar
        403.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [epost, losenord]
              properties:
                epost: { type: string, format: email }
                losenord: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Inloggad" }
        "401": { $ref: "#/components/responses/Fel" }
        "403":
          description: Kontot är avstängt.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }
        "429":
          description: För många misslyckade försök.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }

  /api/anvandare:
    get:
      tags: [Användare]
      summary: Lista organisationens användare
      description: Kräver rollen `admin` eller `arbetsledare`.
      responses:
        "200":
          description: Användare i den egna organisationen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  anvandare:
                    type: array
                    items: { $ref: "#/components/schemas/Anvandare" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
    post:
      tags: [Användare]
      summary: Skapa användare i den egna organisationen
      description: Kräver rollen `admin`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [epost, losenord, namn, roll]
              properties:
                epost: { type: string, format: email }
                losenord: { type: string, minLength: 8 }
                namn: { type: string }
                roll: { $ref: "#/components/schemas/Roll" }
      responses:
        "200":
          description: Den skapade användaren.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Anvandare" }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "409": { $ref: "#/components/responses/Fel" }

  /api/ecm/regler:
    get:
      tags: [Drift]
      summary: ECM Knowledge Library — aktuellt regelpaket
      description: >
        Serverdistribuerat, versionerat regelpaket (compliance-regler per
        ärendetyp, orsakskategorier, evidenskällor, undantagsorsaker).
        Uppdateras i driften utan appändring; klienten cachar och faller
        tillbaka till sitt inbyggda standardpaket offline.
      responses:
        "200":
          description: Regelpaketet.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string }
                  arendetypRegler: { type: object, additionalProperties: true }
                  undantagsorsaker: { type: array, items: { type: string } }
                  orsakskategorier: { type: array, items: { type: string } }
                  underlagskallor: { type: array, items: { type: string } }

  /api/fordon/{identifierare}/historik:
    get:
      tags: [Ärenden]
      summary: Fordonshistorik — tidigare ärenden på samma objekt
      description: >
        Organisationens tidigare ärenden där objektet (regnr/VIN) matchar,
        med dokumenterade felorsaker. Underlag för pre-diagnostikens
        historiksteg och orsakskedjan.
      parameters:
        - name: identifierare
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Tidigare ärenden (senaste 20).
          content:
            application/json:
              schema:
                type: object
                properties:
                  arenden:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        nummer: { type: integer }
                        skapad: { type: string, format: date-time }
                        avslutat: { type: boolean }
                        felbeskrivning: { type: string, nullable: true }
                        felorsaker: { type: array, items: { type: object, additionalProperties: true } }
        "401": { $ref: "#/components/responses/Fel" }

  /api/statistik/felorsaker:
    get:
      tags: [Översikt]
      summary: Felorsaksstatistik per orsakskategori
      description: >
        Flottdata ur dokumenterade felorsaksanalyser i organisationen.
        Kräver rollen `arbetsledare` eller `admin`.
      responses:
        "200":
          description: Antal per orsakskategori, fallande.
          content:
            application/json:
              schema:
                type: object
                properties:
                  orsaker:
                    type: array
                    items:
                      type: object
                      properties:
                        orsak: { type: string }
                        antal: { type: integer }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }

  /api/anvandare/{anvandarId}/avaktivera:
    post:
      tags: [Användare]
      summary: Stäng av ett konto
      description: >
        Kräver rollen `admin` och att användaren tillhör samma
        organisation. Avstängningen höjer kontots token-version, så
        **pågående sessioner upphör omedelbart** — annars vore den
        verkningslös tills utfärdade tokens gick ut. Ett konto kan inte
        stänga av sig självt.
      parameters:
        - name: anvandarId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Kontot är avstängt.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  namn: { type: string }
                  aktiv: { type: boolean }
        "400":
          description: Försök att stänga av sitt eget konto.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/anvandare/{anvandarId}/aktivera:
    post:
      tags: [Användare]
      summary: Öppna ett avstängt konto
      description: >
        Kräver rollen `admin`. Kontot kan logga in igen, men tokens som
        återkallades vid avstängningen förblir ogiltiga.
      parameters:
        - name: anvandarId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: Kontot är öppnat. }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/auth/logga-ut-alla:
    post:
      tags: [Auth]
      summary: Logga ut på alla enheter
      description: >
        Höjer den egna token-versionen, vilket gör samtliga utfärdade
        tokens för kontot ogiltiga direkt — vägen ut när en enhet
        tappats bort.
      responses:
        "200": { description: Alla sessioner är avslutade. }
        "401": { $ref: "#/components/responses/Fel" }

  /api/arenden/{arendeId}/bilagor:
    post:
      tags: [Ärenden]
      summary: Ladda upp en bilaga
      description: >
        Foton, videoklipp och instrumentbilder. Kroppen är råa bytes och
        `Content-Type` anger mediatypen — endast bild och video tas emot.
        Servern beräknar innehållets SHA-256 och returnerar en referens
        som ska läggas i händelsen; **hashen hamnar därmed i den
        append-only-skyddade loggen**, så en utbytt bild går att upptäcka.
        Innehållsadresserat: samma innehåll lagras en gång.
      parameters:
        - name: arendeId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          image/jpeg: { schema: { type: string, format: binary } }
          image/png: { schema: { type: string, format: binary } }
          image/webp: { schema: { type: string, format: binary } }
          video/mp4: { schema: { type: string, format: binary } }
          video/webm: { schema: { type: string, format: binary } }
      responses:
        "200":
          description: Referensen att spara i händelsen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  hash: { type: string, description: "SHA-256 av innehållet, hex." }
                  mediatyp: { type: string }
                  storlek: { type: integer }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
        "413":
          description: Bilagan är större än 32 MB.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }
        "415":
          description: Endast bilder och videoklipp tas emot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }

  /api/bilagor/{bilagaId}:
    get:
      tags: [Ärenden]
      summary: Hämta en bilaga
      description: >
        Organisationsknuten. Innehållet kontrolleras mot hashen innan det
        lämnas ut — stämmer det inte svarar tjänsten 409 i stället för att
        visa en bild som kan ha bytts ut.
      parameters:
        - name: bilagaId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Innehållet.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
        "409":
          description: Innehållet stämmer inte med hashen i loggen.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }

  /api/delad/{delningskod}/bilagor/{bilagaId}:
    get:
      tags: [Delning]
      summary: Hämta en bilaga via delningslänk
      description: >
        Samma filtrering som händelserna: bilagan lämnas bara ut om
        händelsen den hör till är synlig på delningens nivå. En bild som
        hör till en intern händelsetyp — t.ex. den skannade arbetsordern —
        nås alltså aldrig via kundlänken.
      security: []
      parameters:
        - name: delningskod
          in: path
          required: true
          schema: { type: string }
        - name: bilagaId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Innehållet.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "404": { $ref: "#/components/responses/Fel" }
        "409": { $ref: "#/components/responses/Fel" }

  /api/integrationer/leverantorer:
    get:
      tags: [Integrationer]
      summary: Registret över märkesspecifika kopplingar
      description: >
        Leverantörer är data, inte kod: registret läses ur
        `integrationer.json` (eller filen i `INTEGRATIONER_FIL`) och kan
        bytas via ConfigMap utan att applikationen byggs om. Innehåller
        endast fältdefinitioner — aldrig någon organisations uppgifter.
      responses:
        "200":
          description: Leverantörsdefinitioner.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string }
                  leverantorer:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        namn: { type: string }
                        beskrivning: { type: string }
                        nyckeltyp:
                          type: string
                          enum: [vin, regnr]
                          description: Vad uppslaget sker på. Utelämnat betyder VIN.
                        falt:
                          type: array
                          items:
                            type: object
                            properties:
                              nyckel: { type: string }
                              etikett: { type: string }
                              hemlig:
                                type: boolean
                                description: >
                                  Hemliga fält maskeras alltid i svar och
                                  visas aldrig igen efter sparande.
        "401": { $ref: "#/components/responses/Fel" }

  /api/integrationer:
    get:
      tags: [Integrationer]
      summary: Organisationens konfigurerade kopplingar
      description: >
        Kräver rollen `admin`. Uppgifterna lagras krypterade
        (AES-256-GCM) och returneras alltid maskerade — hemliga värden
        lämnar aldrig servern i klartext.
      responses:
        "200":
          description: Konfigurerade kopplingar med maskerade uppgifter.
          content:
            application/json:
              schema:
                type: object
                properties:
                  krypteringKonfigurerad:
                    type: boolean
                    description: Falskt om `INTEGRATION_NYCKEL` saknas — då kan inget sparas.
                  integrationer:
                    type: array
                    items:
                      type: object
                      properties:
                        leverantor: { type: string }
                        namn: { type: string }
                        aktiv: { type: boolean }
                        uppdaterad: { type: string, format: date-time }
                        senast_testad: { type: string, format: date-time, nullable: true }
                        senaste_status: { type: string, nullable: true }
                        uppgifter:
                          type: object
                          additionalProperties: { type: string }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
    post:
      tags: [Integrationer]
      summary: Spara eller uppdatera en kopplings uppgifter
      description: >
        Kräver rollen `admin`. Endast leverantörens definierade fält
        sparas och samtliga måste fyllas i. Uppgifterna krypteras innan
        de skrivs. Ett sparande nollställer tidigare testresultat.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [leverantor, uppgifter]
              properties:
                leverantor: { type: string }
                aktiv: { type: boolean, default: true }
                uppgifter:
                  type: object
                  additionalProperties: { type: string }
      responses:
        "200": { description: Sparad. }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "503":
          description: Kryptering är inte konfigurerad (`INTEGRATION_NYCKEL` saknas).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }

  /api/integrationer/{leverantor}:
    delete:
      tags: [Integrationer]
      summary: Ta bort en kopplings uppgifter
      description: Kräver rollen `admin`.
      parameters:
        - name: leverantor
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Borttagen. }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }

  /api/integrationer/{leverantor}/uppslag:
    post:
      tags: [Integrationer]
      summary: Slå upp ett fordon hos leverantören
      description: >
        Anropet görs alltid av servern — kundens leverantörsnycklar når
        aldrig webbläsaren. Svaret mappas till våra fält enligt
        registrets `svarsfalt`. Resultatet skrivs som kopplingens
        senaste teststatus.
      parameters:
        - name: leverantor
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [identifierare]
              properties:
                identifierare:
                  type: string
                  description: VIN eller registreringsnummer beroende på leverantörens `nyckeltyp`.
      responses:
        "200":
          description: Fordonsuppgifter från leverantören.
          content:
            application/json:
              schema:
                type: object
                properties:
                  fordon:
                    type: object
                    additionalProperties: { type: string }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "404":
          description: Kopplingen är inte konfigurerad för organisationen.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }
        "502":
          description: Leverantören svarade med fel eller inga kända fält.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }
        "503":
          description: Kryptering är inte konfigurerad.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fel" }

  /api/delad/{delningskod}/beslut:
    post:
      tags: [Delning]
      summary: Kundens besked på ett åtgärdsförslag
      description: >
        Den enda skrivande publika vägen. Endast delningar på **kundnivå**
        som inte återkallats får svara, det måste finnas ett
        åtgärdsförslag, och **ett besked per ärende** — svaret kan inte
        ändras i efterhand. Takt-begränsad per delningskod. Beskedet
        loggas som `kundbeslut` med kanal `Delningslänk`.
      security: []
      parameters:
        - name: delningskod
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [beslut]
              properties:
                beslut: { type: string, enum: [godkant, avbojt, delvis] }
                kommentar: { type: string, maxLength: 500 }
      responses:
        "200":
          description: Beskedet registrerat.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
        "409": { $ref: "#/components/responses/Fel" }
        "429": { $ref: "#/components/responses/Fel" }

  /api/organisation:
    get:
      tags: [Organisation]
      summary: Hämta organisationens namn och inställningar
      description: >
        Inställningarna styr vad som visas när ett ärende startas
        (objekttyper och identifieringsmetoder). Alla inloggade läser.
      responses:
        "200":
          description: Organisationen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  namn: { type: string }
                  installningar: { $ref: "#/components/schemas/OrganisationsInstallningar" }
        "401": { $ref: "#/components/responses/Fel" }

  /api/organisation/installningar:
    post:
      tags: [Organisation]
      summary: Uppdatera organisationens inställningar
      description: Kräver rollen `admin`. Minst ett alternativ per lista.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrganisationsInstallningar" }
      responses:
        "200":
          description: Sparat.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }

  /api/arenden:
    get:
      tags: [Ärenden]
      summary: Lista organisationens ärenden
      responses:
        "200":
          description: Ärenden (senaste 200).
          content:
            application/json:
              schema:
                type: object
                properties:
                  arenden:
                    type: array
                    items: { $ref: "#/components/schemas/Arende" }
        "401": { $ref: "#/components/responses/Fel" }
    post:
      tags: [Ärenden]
      summary: Registrera ett ärende (idempotent)
      description: >
        Skapar ärendet i användarens organisation. Ett redan känt `id`
        ignoreras tyst — ärenderaden ändras aldrig.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, nummer, skapad]
              properties:
                id: { type: string }
                nummer: { type: integer }
                skapad: { type: string, format: date-time }
                delningskod: { type: string, nullable: true }
                metodikId: { type: string, nullable: true }
      responses:
        "200":
          description: Mottaget.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }

  /api/arenden/{arendeId}/handelser:
    parameters:
      - name: arendeId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Ärenden]
      summary: Hämta ärendets händelselogg
      responses:
        "200":
          description: Händelser i tidsordning.
          content:
            application/json:
              schema:
                type: object
                properties:
                  handelser:
                    type: array
                    items: { $ref: "#/components/schemas/LoggPost" }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
    post:
      tags: [Ärenden]
      summary: Lägg till händelser (append-only, idempotent)
      description: >
        Lägger till händelser i loggen. Max 500 per anrop. En händelse med
        redan känt `id` ignoreras tyst — befintliga händelser skrivs aldrig
        över, och databastriggern stoppar alla ändringsförsök.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [handelser]
              properties:
                handelser:
                  type: array
                  maxItems: 500
                  items: { $ref: "#/components/schemas/LoggPost" }
      responses:
        "200":
          description: Mottaget.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/oversikt:
    get:
      tags: [Översikt]
      summary: Organisationsöversikt med status och statistikunderlag
      description: >
        Kräver rollen `arbetsledare` eller `admin`. Status, deltagande
        tekniker och sammanfattning härleds ur händelseloggen.
      responses:
        "200":
          description: Alla ärenden i organisationen med härledd status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  arenden:
                    type: array
                    items: { $ref: "#/components/schemas/OversiktsRad" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }

  /api/arenden/{arendeId}/delningar:
    parameters:
      - name: arendeId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Delning]
      summary: Lista ärendets delningslänkar
      responses:
        "200":
          description: Delningar (inklusive återkallade).
          content:
            application/json:
              schema:
                type: object
                properties:
                  delningar:
                    type: array
                    items: { $ref: "#/components/schemas/Delning" }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
    post:
      tags: [Delning]
      summary: Skapa delningslänk med behörighetsnivå
      description: >
        Verkstaden kontrollerar alltid delningen: kund (det kunddelbara),
        partner (även hypoteser, tydligt märkta ej verifierade) eller
        intern (full insyn). Nivåfiltreringen sker på serversidan.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [niva]
              properties:
                niva: { $ref: "#/components/schemas/DelningsNiva" }
      responses:
        "200":
          description: Den skapade länkens kod.
          content:
            application/json:
              schema:
                type: object
                properties:
                  kod: { type: string }
                  niva: { $ref: "#/components/schemas/DelningsNiva" }
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/delningar/{kod}/aterkalla:
    post:
      tags: [Delning]
      summary: Återkalla en delningslänk
      description: Efter återkallelse ger länken 404. Kan inte ångras.
      parameters:
        - name: kod
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Återkallad.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "401": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/delad/{delningskod}:
    get:
      tags: [Delning]
      summary: Publik Live Share-vy via delningskod
      description: >
        Kräver ingen inloggning — delningskoden är nyckeln. Filtreringen
        styrs av länkens behörighetsnivå: kund utesluter kategoribyten,
        hypoteser och AI-dialog; partner utesluter kategoribyten och
        AI-dialog; intern visar allt. Ärendets ursprungliga delningskod
        fungerar bakåtkompatibelt som kundnivå.
      security: []
      parameters:
        - name: delningskod
          in: path
          required: true
          schema: { type: string, pattern: "^[A-Za-z0-9_-]+$" }
      responses:
        "200":
          description: Ärendet med nivåfiltrerad händelselogg.
          content:
            application/json:
              schema:
                type: object
                properties:
                  arende:
                    type: object
                    properties:
                      id: { type: string }
                      nummer: { type: integer }
                      skapad: { type: string, format: date-time }
                  handelser:
                    type: array
                    items: { $ref: "#/components/schemas/LoggPost" }
                  niva: { $ref: "#/components/schemas/DelningsNiva" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/ai:
    post:
      tags: [AI]
      summary: AI-orkestern (separat tjänst bakom samma ingress)
      description: >
        Routas av ingressen till ai-orkester-tjänsten. Servern äger
        modellval, effort, systemprompt och svarsschema per uppgift:
        handledning (Claude Sonnet 5), granskning (Claude Opus 5),
        sammanfattning (Claude Sonnet 5), metodikval (Claude Haiku 4.5).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [uppgift, prompt]
              properties:
                uppgift:
                  type: string
                  enum: [handledning, granskning, sammanfattning, metodikval]
                prompt: { type: string, maxLength: 40000 }
      responses:
        "200":
          description: Klassificerat AI-svar med modellen som svarade.
          content:
            application/json:
              schema:
                type: object
                properties:
                  modell: { type: string, example: claude-sonnet-5 }
                  svar:
                    oneOf:
                      - $ref: "#/components/schemas/AiSvar"
                      - type: object
                        properties:
                          metodikId:
                            type: string
                            enum: [vibration, elsystem, generisk]
        "400": { $ref: "#/components/responses/Fel" }
        "401": { $ref: "#/components/responses/Fel" }
        "502": { $ref: "#/components/responses/Fel" }
        "503": { $ref: "#/components/responses/Fel" }

  /api/arenden/{arendeId}/sammanfattning:
    get:
      summary: Härledd sammanfattning av ärendet (ALVA-PROC-0030)
      description: >
        Några få meningar som ger vem som helst en bild av ärendet.
        Sammanfattningen är HÄRLEDD ur händelseloggen, inte genererad:
        samma ärende ger samma text i dag och om två år, den innehåller
        inget som inte står i loggen, och den säger uttryckligen när något
        saknas i stället för att utelämna det. Avsedd för handläggare,
        kundtjänst och integrationer.
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: arendeId, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Sammanfattning
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string, example: ALVA-PROC-0030 }
                  text: { type: string }
                  meningar: { type: array, items: { type: string } }
                  enrading: { type: string }
                  fullstandig:
                    type: boolean
                    description: Sant när inget obligatoriskt underlag saknas.
                  saknas: { type: array, items: { type: string } }
        "404": { description: Ärendet är inte tillgängligt }

  /api/arenden/{arendeId}/protokoll:
    post:
      summary: Läs in ett diagnosprotokoll som evidens (ALVA-PROC-0020)
      description: >
        Avläsningar blir händelser i loggen, inte en bilaga. Härkomsten
        bevaras i varje post, så ett värde som kommit utifrån aldrig ser ut
        som något teknikern själv mätt. Lämnas instrumentets identitet
        behålls den; utan den graderas värdet E1 i stället för E4.
        Profilen beskriver var värdena ligger i leverantörens format —
        ALVA antar aldrig ett visst leverantörsformat.
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: arendeId, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kalla, profil, protokoll]
              properties:
                kalla:
                  type: string
                  description: Vad som producerade protokollet. Följer med varje händelse.
                  example: Diagnosinstrument, plats 3
                profil:
                  type: object
                  properties:
                    felkoder:
                      type: object
                      properties:
                        vag: { type: string, example: dtcs }
                        kod: { type: string, example: code }
                        text: { type: string, example: description }
                    matvarden:
                      type: object
                      properties:
                        vag: { type: string, example: liveData }
                        beskrivning: { type: string, example: name }
                        varde: { type: string, example: value }
                        enhet: { type: string, example: unit }
                        instrumentId: { type: string, example: toolSerial }
                protokoll:
                  type: object
                  description: Leverantörens nyttolast, oförändrad.
      responses:
        "200":
          description: Antal skrivna händelser
          content:
            application/json:
              schema:
                type: object
                properties:
                  handelser: { type: integer }
                  kalla: { type: string }
        "422": { description: Profilen gav inga händelser ur protokollet }

  /api/statistik/oversikt:
    get:
      summary: Driftmått för organisationen (ALVA-REP-0100)
      description: >
        Samma underlag som portalens analysvy och kvartalsrapporten, så att
        skärm och rapport aldrig visar olika siffror för samma period.
        Ett mått utan underlag returneras som null, aldrig som noll.
        Kräver arbetsledare eller administratör.
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Driftmått
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string, example: ALVA-REP-0100 }
                  verifiering:
                    type: object
                    description: Andel avslut med fastställd orsak.
                    properties:
                      antal: { type: integer }
                      fastställda: { type: integer }
                      andel: { type: number, nullable: true }
                  reproduktion: { type: object }
                  omarbetning:
                    type: object
                    description: Samma fordon tillbaka med samma orsakskategori.
                  undantag:
                    type: array
                    description: Kontroller som oftast hoppas över, per steg och ALVA-fas.
                    items: { type: object }
                  evidens: { type: object }
                  faser: { type: object }
                  orsaker: { type: array, items: { type: object } }
        "403": { description: Kräver arbetsledare eller administratör }

  /api/integration/kategorier:
    get:
      summary: Systemkategorier och utgående händelser (ALVA-SPEC-020)
      description: >
        ALVA är leverantörsoberoende. Kategorierna beskriver vilka slags
        system som kan kopplas in och åt vilket håll data går; en profil
        märks validated först efter att den körts mot leverantörens
        faktiska gränssnitt.
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Kategorier och händelsetyper
          content:
            application/json:
              schema:
                type: object
                properties:
                  kategorier: { type: object }
                  handelser:
                    type: object
                    description: Utgående händelsetyper med beskrivning.

  /api/integration/prenumerationer:
    get:
      summary: Organisationens utgående prenumerationer (ALVA-SPEC-021)
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Prenumerationer }
        "403": { description: Kräver administratörsbehörighet }
    post:
      summary: Registrera en mottagare för utgående händelser
      description: >
        Leveranser signeras med HMAC över tidsstämpel och kropp i huvudet
        `alva-signatur` (`t=<unix>,v1=<hex>`). Tidsstämpeln ligger inne i
        signaturen, så en fångad leverans inte går att spela upp senare;
        toleransen är fem minuter. Adressen kontrolleras mot samma
        SSRF-gräns som leverantörsuppslagen.
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [namn, url, handelser]
              properties:
                namn: { type: string }
                url: { type: string, format: uri }
                handelser:
                  type: array
                  items:
                    type: string
                    enum:
                      - arende.skapat
                      - arende.fas
                      - arende.slutsats
                      - arende.avslutat
                      - media.tillagt
                      - atgardsforslag.lamnat
                      - kundbeslut.registrerat
                hemlighet:
                  type: string
                  description: Utelämnas för att låta servern generera en.
      responses:
        "200": { description: Skapad }
        "400": { description: Ogiltig adress eller okänd händelsetyp }

  /api/radering:
    post:
      summary: Radera personuppgifter för ett fordon (krypto-shredding)
      description: >
        Nyckeln förstörs; loggen står kvar. Det som raderas är
        identifieringen, inte protokollet över vad som kontrollerades —
        den enda konstruktion där bevisvärdet överlever en
        raderingsbegäran. Kräver en bekräftelse som upprepar subjektet
        exakt; åtgärden går inte att ångra.
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subjekt, bekraftelse]
              properties:
                subjekt: { type: string, description: Registreringsnummer eller ärende-id. }
                bekraftelse: { type: string, description: Måste vara identisk med subjekt. }
      responses:
        "200": { description: Verkställd }
        "400": { description: Bekräftelsen matchar inte }
        "404": { description: Inget skyddat underlag finns }

  /api/matdon:
    get:
      summary: Organisationens mätdon med kalibreringsstatus (ALVA-SPEC-004)
      description: >
        Ett mätvärde graderas E4 endast med spårbart, kalibrerat
        instrument. Utan register är påståendet inte kontrollerbart.
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Mätdon }
    post:
      summary: Registrera eller uppdatera ett mätdon
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Sparat }
        "403": { description: Kräver arbetsledare eller administratör }

  /api/atkomstlogg:
    get:
      tags: [Drift]
      summary: Åtkomstlogg — vem som läst vilket ärende
      description: >
        Kräver `arbetsledare` eller `admin`, och visar endast den egna
        organisationen. Loggen omfattar även läsningar via publika
        delningslänkar, med koden angiven. Tabellen är append-only: posterna
        kan varken ändras eller raderas, eftersom de utgör bevisningen för
        att åtkomststyrningen fungerade.
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: De 500 senaste åtkomsterna.
          content:
            application/json:
              schema:
                type: object
                properties:
                  atkomster:
                    type: array
                    items:
                      type: object
                      properties:
                        tidpunkt: { type: string, format: date-time }
                        arende_id: { type: string, nullable: true }
                        vag: { type: string }
                        anvandare: { type: string, nullable: true }
                        delningskod: { type: string, nullable: true }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }

  # ---- Fakturering (ALVA-PROC-0001) ------------------------------------
  #
  # Två parter, inte en. Organisationen LÄSER sina fakturor; utfärdaren
  # skapar dem och registrerar betalning. Utfärdaren är inte en användare
  # i någon organisation och identifieras med en egen nyckel
  # (X-Fakturering) — en kund ska inte kunna bokföra sin egen betalning.
  #
  # Ingen betalleverantör är inblandad och inga kortuppgifter hanteras.
  /api/fakturor:
    get:
      tags: [Fakturering]
      summary: Organisationens egna fakturor
      description: >
        Kräver rollen `admin`. Statusen är en projektion av
        fakturahändelserna, inte ett lagrat fält — en utfärdad faktura
        ändras aldrig.
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Fakturor, senast utfärdad först.
          content:
            application/json:
              schema:
                type: object
                properties:
                  fakturor:
                    type: array
                    items: { $ref: "#/components/schemas/Faktura" }
        "401": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
    post:
      tags: [Fakturering]
      summary: Utfärda en faktura
      description: >
        Kräver utfärdarens nyckel i `X-Fakturering`. Beloppet kan inte
        anges: det härleds ur organisationens faktiska tillstånd — antalet
        aktiva konton, licensperioden, påslagna moduler. Ett anrop som
        ändå innehåller `rader`, `totalt` eller `belopp` avvisas med 400.
      security: [{ utfardarNyckel: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [organisation_id, period]
              properties:
                organisation_id: { type: string, format: uuid }
                period:
                  type: object
                  required: [fran, till]
                  properties:
                    fran: { type: string, format: date }
                    till: { type: string, format: date }
                utfardad:
                  type: string
                  format: date
                  description: Standard är dagens datum.
      responses:
        "201":
          description: Fakturan, fullständig och oföränderlig.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Faktura" }
        "400": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }

  /api/fakturor/{fakturaId}/betald:
    post:
      tags: [Fakturering]
      summary: Registrera betalning
      description: >
        Kräver utfärdarens nyckel. Ingen uppdatering sker — betalningen
        skrivs som en egen händelse, och statusen härleds ur den.
        Plattformen registrerar aldrig en betalning av sig själv.
      security: [{ utfardarNyckel: [] }]
      parameters:
        - { name: fakturaId, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [referens]
              properties:
                referens:
                  type: string
                  description: Betalningsreferens, minst 3 tecken — annars går betalningen inte att spåra.
                betaldatum: { type: string, format: date }
      responses:
        "200": { description: Registrerad. }
        "400": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
        "409": { description: Fakturan är redan betald eller krediterad. }

  /api/fakturor/{fakturaId}/kreditera:
    post:
      tags: [Fakturering]
      summary: Kreditera en faktura
      description: >
        Kräver utfärdarens nyckel. En felaktig faktura rättas inte — den
        bemöts av en kreditfaktura som pekar tillbaka på den, med omvänt
        tecken och ett granskbart skäl (minst 10 tecken).
      security: [{ utfardarNyckel: [] }]
      parameters:
        - { name: fakturaId, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [orsak]
              properties:
                orsak: { type: string, minLength: 10 }
                utfardad: { type: string, format: date }
      responses:
        "201":
          description: Kreditfakturan.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Faktura" }
        "400": { $ref: "#/components/responses/Fel" }
        "403": { $ref: "#/components/responses/Fel" }
        "404": { $ref: "#/components/responses/Fel" }
        "409": { description: Fakturan är redan krediterad. }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        HS256-JWT från /api/auth/logga-in eller /api/auth/registrera.
        Anspråk: sub (användar-id), namn, org (organisations-id),
        roll, iat, exp (12 timmar).
    utfardarNyckel:
      type: apiKey
      in: header
      name: X-Fakturering
      description: >
        Utfärdarens nyckel (FAKTURERING_NYCKEL). Fakturering är en
        relation mellan installationen och kunden — ingen av rollerna i
        en verkstad är motpart i det avtalet, så en organisations
        administratör kan varken utfärda sin egen faktura eller bokföra
        den som betald. Saknas nyckeln i miljön kan ingen faktura
        utfärdas alls: faktureringen fallerar stängt.
  responses:
    Inloggad:
      description: Inloggad — token + kontouppgifter.
      content:
        application/json:
          schema:
            type: object
            properties:
              token: { type: string }
              namn: { type: string }
              roll: { $ref: "#/components/schemas/Roll" }
              organisation: { type: string }
    Fel:
      description: Felsvar.
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
  schemas:
    Fel:
      type: object
      properties:
        error: { type: string }
    Faktura:
      type: object
      description: >
        Belopp i minsta valutaenhet (öre). Varje rad bär sitt underlag —
        var antalet kommer ifrån — så att summan går att granska utan att
        fråga någon. `status` är härledd, inte lagrad.
      properties:
        id: { type: string, format: uuid }
        beteckning: { type: string, example: ALVA-INV-0001 }
        organisation: { type: string }
        period:
          type: object
          properties:
            fran: { type: string, format: date }
            till: { type: string, format: date }
        utfardad: { type: string, format: date }
        forfaller: { type: string, format: date }
        valuta: { type: string, example: SEK }
        rader:
          type: array
          items:
            type: object
            properties:
              benamning: { type: string }
              underlag: { type: string }
              antal: { type: integer }
              enhet: { type: string }
              apris: { type: integer }
              belopp: { type: integer }
        netto: { type: integer }
        momssats: { type: number }
        moms: { type: integer }
        totalt: { type: integer }
        krediterar:
          type: string
          nullable: true
          description: Beteckningen på den faktura denna kreditfaktura rättar.
        status:
          type: string
          enum: [utfardad, betald, krediterad]
        betalningssatt: { type: string }
    Roll:
      type: string
      enum: [tekniker, arbetsledare, admin]
    OrganisationsInstallningar:
      type: object
      required: [objekttyper, identifieringsmetoder]
      properties:
        objekttyper:
          type: array
          minItems: 1
          items: { type: string }
        identifieringsmetoder:
          type: array
          minItems: 1
          items: { type: string }
    DelningsNiva:
      type: string
      enum: [kund, partner, intern]
    Delning:
      type: object
      properties:
        kod: { type: string }
        niva: { $ref: "#/components/schemas/DelningsNiva" }
        skapad: { type: string, format: date-time }
        aterkallad: { type: string, format: date-time, nullable: true }
    Anvandare:
      type: object
      properties:
        id: { type: string, format: uuid }
        epost: { type: string, format: email }
        namn: { type: string }
        roll: { $ref: "#/components/schemas/Roll" }
    Arende:
      type: object
      properties:
        id: { type: string }
        nummer: { type: integer }
        skapad: { type: string, format: date-time }
        delningskod: { type: string, nullable: true }
        metodik_id: { type: string, nullable: true }
    LoggPost:
      type: object
      required: [id, tidpunkt, anvandare, handelse]
      properties:
        id: { type: string }
        tidpunkt: { type: string, format: date-time }
        anvandare: { type: string }
        handelse: { $ref: "#/components/schemas/Handelse" }
    Handelse:
      type: object
      description: >
        Diskriminerad på `typ`; fullständiga fältdefinitioner i klientens
        domänmodell (src/felsokning/domain.ts).
      required: [typ]
      additionalProperties: true
      properties:
        typ:
          type: string
          enum:
            - objekt_identifierat
            - felbeskrivning
            - fraga_besvarad
            - kontroll_utford
            - observation
            - matvarde
            - hypotes
            - foto
            - kommentar
            - kategori_byte
            - inaktivitet_forklarad
            - overlamning
            - ansvarig_satt
            - arbetsorder_skannad
            - arendetyp_satt
            - historik_kontrollerad
            - matarstallning
            - video
            - atgardsforslag
            - kundbeslut
            - atgard_utford
            - kvalitetskontroll
            - reproducering
            - felorsak
            - export_skapad
            - ai_svar
            - arende_avslutat
    OversiktsRad:
      type: object
      properties:
        id: { type: string }
        nummer: { type: integer }
        skapad: { type: string, format: date-time }
        delningskod: { type: string, nullable: true }
        metodik_id: { type: string, nullable: true }
        antal_handelser: { type: integer }
        forsta: { type: string, format: date-time, nullable: true }
        senaste: { type: string, format: date-time, nullable: true }
        avslutat: { type: boolean }
        objekt: { type: string, nullable: true }
        felbeskrivning: { type: string, nullable: true }
        tekniker:
          type: array
          items: { type: string }
          nullable: true
    AiSvar:
      type: object
      required: [rader, nastaSteg]
      properties:
        rader:
          type: array
          items:
            type: object
            required: [typ, text]
            properties:
              typ:
                type: string
                enum: [observation, verifierat, hypotes, rekommendation]
              text: { type: string }
        nastaSteg: { type: string }

security:
  - bearerAuth: []
