openapi: 3.1.0

x-document-control:
  stand: "10.08.2026 20:00"
  branch: main
  status: verbindlich
  change_history:
    - version: "1.0.2"
      date_time: "10.08.2026 20:00"
      change: "Sicheren UC00-Merge-Stand nach main übernommen und Branch-Nachführung ergänzt"
      author: Codex
    - version: "1.0.1"
      date_time: "10.08.2026 19:17"
      change: "Plan-Typ bei der Tenant-Vorbereitung verpflichtend gemacht"
      author: Codex
    - version: "1.0.0"
      date_time: "09.08.2026 15:33"
      change: "Verbindlichen UC00-API-Vertrag aus Fachregeln, ADRs, Prisma-v4-Zielschema und Nachverfolgbarkeitsmatrix konsolidiert"
      author: Codex

info:
  title: "DiveLogix360 API – UC00 Tenant-Onboarding und Schulverwaltung"
  version: 1.0.2
  contact:
    name: DiveLogix360 Support
    url: https://divelogix360.ch
  description: |
    Verbindlicher OpenAPI-3.1-Vertrag fuer UC00. Die vier aelteren Blockdateien
    unter `docs/api/` bleiben als historische Arbeitsstaende erhalten.

    Fachlich gelten insbesondere:

    - globale, getrimmte und kleingeschriebene E-Mail-Adressen;
    - vorbereitende Tenant-Anlage vor Vertragsabschluss;
    - Auftragsverarbeitungsvertrag vor Tenant-Admin-Einladung;
    - elektronische Vertragslinks mit sieben Tagen Laufzeit;
    - Tenant-Admin-Einladungen mit 72 Stunden Laufzeit;
    - Aktivierung administrativer Benutzer erst nach erfolgreicher Zwei-Faktor-Authentifizierung;
    - genau vier persistente Onboarding-Schritte;
    - sofortige Offboarding-Sperrung, 30 Tage Exportzugang und operative
      Loeschung spaetestens nach 90 Tagen.

servers:
  - url: https://api.divelogix360.ch/v1
    description: Produktion
  - url: https://api-dev.divelogix360.ch/v1
    description: Entwicklung und Staging

tags:
  - name: Auth
  - name: Invitation
  - name: Superadmin Tenants
  - name: Contracts
  - name: Tenant Profile
  - name: Onboarding
  - name: Users
  - name: Offboarding
  - name: Exports

paths:
  /auth/login:
    post:
      tags: [Auth]
      summary: Anmeldung beginnen
      operationId: uc00AuthLogin
      security: []
      x-uc00-rules: [G1, G36, G38, G39, G43, G44]
      x-test-cases: [API-UC00-01, API-UC00-02, API-UC00-03]
      description: |
        Die E-Mail-Adresse wird vor der Suche getrimmt und kleingeschrieben.
        Fehler bleiben absichtlich generisch. Ein erforderlicher zweiter Faktor
        wird ueber eine gehashte, einmalige und hoechstens zehn Minuten gueltige
        Sitzung fortgesetzt. `lastLoginAt` wird hier noch nicht aktualisiert.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Anmeldung begonnen oder ohne erforderlichen zweiten Faktor abgeschlossen
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginStartResponse'
        '401':
          $ref: '#/components/responses/InvalidCredentials'
        '423':
          $ref: '#/components/responses/AccountLocked'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /auth/login/2fa:
    post:
      tags: [Auth]
      summary: Anmeldung mit TOTP abschliessen
      operationId: uc00AuthLoginTwoFactor
      security: []
      x-uc00-rules: [G40, G43, G44]
      x-test-cases: [API-UC00-04, API-UC00-05]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwoFactorLoginRequest'
      responses:
        '200':
          $ref: '#/components/responses/LoginCompleted'
        '400':
          $ref: '#/components/responses/InvalidTwoFactorSession'
        '401':
          $ref: '#/components/responses/InvalidTwoFactorCode'
        '423':
          $ref: '#/components/responses/AccountLocked'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /auth/login/recovery:
    post:
      tags: [Auth]
      summary: Anmeldung mit Wiederherstellungscode abschliessen
      operationId: uc00AuthLoginRecovery
      security: []
      x-uc00-rules: [G40, G42, G44, G70]
      x-test-cases: [API-UC00-06, API-UC00-07]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecoveryLoginRequest'
      responses:
        '200':
          $ref: '#/components/responses/LoginCompleted'
        '400':
          $ref: '#/components/responses/InvalidTwoFactorSession'
        '401':
          description: Wiederherstellungscode ungueltig oder bereits verwendet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: invalid_recovery_code
                message: Anmeldung nicht moeglich.
                status: 401
                request_id: 957550a9-dc3d-40ef-a367-348c255176cb
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /auth/token/refresh:
    post:
      tags: [Auth]
      summary: Sitzung rotieren
      operationId: uc00AuthRefresh
      security: []
      x-uc00-rules: [G45]
      x-test-cases: [API-UC00-08, API-UC00-09]
      description: |
        Liest den Refresh-Token ausschliesslich aus einem sicheren HttpOnly-Cookie.
        Der Token wird bei jeder Verwendung rotiert. Wiederverwendung widerruft
        die betroffene Tokenfamilie.
      responses:
        '200':
          $ref: '#/components/responses/LoginCompleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Wiederverwendung erkannt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: token_reuse_detected
                message: Sitzung wurde widerrufen. Bitte erneut anmelden.
                status: 403
                request_id: 2dcd9431-ea4a-401c-915d-45d8a7beffce

  /auth/logout:
    post:
      tags: [Auth]
      summary: Sitzung beenden
      operationId: uc00AuthLogout
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-10]
      responses:
        '204':
          description: Sitzung und Refresh-Token widerrufen
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/invite/{token}:
    get:
      tags: [Invitation]
      summary: Einladungslink pruefen
      operationId: uc00InvitationValidate
      security: []
      x-uc00-rules: [G37, G41, G43]
      x-test-cases: [API-UC00-11, API-UC00-12]
      parameters:
        - $ref: '#/components/parameters/InvitationToken'
      responses:
        '200':
          description: Gueltige Einladung; Antwort darf nicht zwischengespeichert werden
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationContext'
        '410':
          $ref: '#/components/responses/InvitationGone'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /auth/invite/{token}/accept:
    post:
      tags: [Invitation]
      summary: Einladung mit Passwort und erforderlicher 2FA annehmen
      operationId: uc00InvitationAccept
      security: []
      x-uc00-rules: [G37, G39, G40, G41, G42, G44]
      x-test-cases: [API-UC00-13, API-UC00-14, API-UC00-15]
      description: |
        Der Benutzer bleibt bis zum erfolgreichen zweiten Faktor im Status
        `invited`. Passwort, verschluesseltes TOTP-Geheimnis, Aktivierung,
        Tokenverwendung und Audit werden atomar gespeichert. Wiederherstellungscodes
        werden nur in dieser Antwort einmalig angezeigt und niemals per E-Mail versendet.
      parameters:
        - $ref: '#/components/parameters/InvitationToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvitationAcceptRequest'
      responses:
        '200':
          description: Benutzer aktiviert
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationAcceptResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '410':
          $ref: '#/components/responses/InvitationGone'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /superadmin/tenants:
    get:
      tags: [Superadmin Tenants]
      summary: Tenants auflisten
      operationId: uc00SuperadminListTenants
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-16]
      parameters:
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/TenantStatus'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Tenantliste
          content:
            application/json:
              schema:
                type: object
                required: [items, total]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/TenantSummary'
                  total:
                    type: integer
                    minimum: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [Superadmin Tenants]
      summary: Tenant fuer Vertragsvorbereitung anlegen
      operationId: uc00SuperadminPrepareTenant
      security:
        - BearerAuth: []
      x-uc00-rules: [G15, G18, G19, G20, G21, G24, G25]
      x-test-cases: [API-UC00-17, API-UC00-18]
      description: |
        Legt nur Tenant und Vertragskontakt im Status `contract_pending` an.
        Es wird weder ein Benutzer noch eine Tenant-Admin-Einladung erzeugt.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantPreparationRequest'
      responses:
        '201':
          description: Tenant vorbereitet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantDetail'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Slug-Konflikt nach interner Wiederholung
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /superadmin/tenants/{tenantSlug}:
    get:
      tags: [Superadmin Tenants]
      summary: Tenantdetails abrufen
      operationId: uc00SuperadminGetTenant
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-19]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Tenantdetails
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /superadmin/tenants/{tenantSlug}/contract-party:
    patch:
      tags: [Superadmin Tenants]
      summary: Vertragspartei vervollstaendigen
      operationId: uc00SuperadminUpdateContractParty
      security:
        - BearerAuth: []
      x-uc00-rules: [G23, G24, G25, G26, G27]
      x-test-cases: [API-UC00-20, API-UC00-21]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContractPartyRequest'
      responses:
        '200':
          description: Vertragspartei gespeichert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantDetail'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ContractAlreadyAccepted'

  /superadmin/tenants/{tenantSlug}/status:
    patch:
      tags: [Superadmin Tenants]
      summary: Tenant sperren oder freigeben
      operationId: uc00SuperadminUpdateTenantStatus
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-22]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status, reason_code]
              properties:
                status:
                  type: string
                  enum: [active, suspended]
                reason_code:
                  type: string
                  minLength: 1
                  maxLength: 100
      responses:
        '200':
          description: Status aktualisiert und Sitzungen bei Sperrung widerrufen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantDetail'
        '409':
          description: Fachlich unzulaessiger Statuswechsel
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /superadmin/tenants/{tenantSlug}/contracts:
    get:
      tags: [Contracts]
      summary: Vertragsversionen abrufen
      operationId: uc00SuperadminListContracts
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-23]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Versionierte Vertragsliste
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ContractRecord'
        '404':
          $ref: '#/components/responses/NotFound'

  /superadmin/tenants/{tenantSlug}/contracts/request:
    post:
      tags: [Contracts]
      summary: Elektronischen Vertragsabschluss anfordern
      operationId: uc00SuperadminRequestContract
      security:
        - BearerAuth: []
      x-uc00-rules: [G23, G24, G26, G29, G30, G31]
      x-test-cases: [API-UC00-24, API-UC00-25, API-UC00-26]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContractRequest'
      responses:
        '201':
          description: Vertragsinstanz und gehashter Sieben-Tage-Link erzeugt; Versand angestossen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractRequestResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ContractPartyIncomplete'

  /contracts/{token}:
    get:
      tags: [Contracts]
      summary: Elektronischen Vertragslink pruefen
      operationId: uc00ValidateContractLink
      security: []
      x-uc00-rules: [G30, G33]
      x-test-cases: [API-UC00-27, API-UC00-28]
      parameters:
        - $ref: '#/components/parameters/ContractToken'
      responses:
        '200':
          description: Vertrag zur Anzeige; Antwort nicht zwischenspeichern
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContract'
        '410':
          $ref: '#/components/responses/ContractLinkGone'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /contracts/{token}/accept:
    post:
      tags: [Contracts]
      summary: Vertrag elektronisch akzeptieren
      operationId: uc00AcceptContract
      security: []
      x-uc00-rules: [G29, G30, G31, G33, G34, G35]
      x-test-cases: [API-UC00-29, API-UC00-30, API-UC00-31]
      parameters:
        - $ref: '#/components/parameters/ContractToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContractAcceptanceRequest'
      responses:
        '200':
          description: Vertrag dauerhaft gespeichert und akzeptiert
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractRecord'
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          $ref: '#/components/responses/ContractPartyIncomplete'
        '410':
          $ref: '#/components/responses/ContractLinkGone'

  /superadmin/tenants/{tenantSlug}/contracts/upload:
    post:
      tags: [Contracts]
      summary: Extern unterzeichnetes Vertrags-PDF hochladen
      operationId: uc00SuperadminUploadContract
      security:
        - BearerAuth: []
      x-uc00-rules: [G32, G34, G35, G89, G90]
      x-test-cases: [API-UC00-32, API-UC00-33, API-UC00-34]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ContractUploadRequest'
      responses:
        '202':
          description: Datei quarantänisiert; Malware-Pruefung laeuft
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractRecord'
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          $ref: '#/components/responses/ContractPartyIncomplete'
        '413':
          $ref: '#/components/responses/FileTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedPdf'
        '503':
          description: Malware-Pruefung nicht verfuegbar; keine Freigabe
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /superadmin/tenants/{tenantSlug}/contracts/{contractId}/review:
    post:
      tags: [Contracts]
      summary: Externen Vertragsnachweis pruefen
      operationId: uc00SuperadminReviewContract
      security:
        - BearerAuth: []
      x-uc00-rules: [G31, G32, G35]
      x-test-cases: [API-UC00-35, API-UC00-36]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ContractId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContractReviewRequest'
      responses:
        '200':
          description: Nachweis akzeptiert oder abgelehnt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractRecord'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Dokument nicht malwarefrei oder bereits abschliessend geprueft
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /superadmin/tenants/{tenantSlug}/admin-invitation:
    post:
      tags: [Invitation]
      summary: Ersten Tenant-Admin einladen
      operationId: uc00SuperadminInviteTenantAdmin
      security:
        - BearerAuth: []
      x-uc00-rules: [G15, G17, G22, G36, G41]
      x-test-cases: [API-UC00-37, API-UC00-38, API-UC00-39]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminInvitationRequest'
      responses:
        '201':
          description: Benutzer im Status invited und gehashte Einladung erzeugt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationResult'
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          description: Vertrag fehlt oder E-Mail ist global belegt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                contract_not_accepted:
                  value:
                    code: contract_not_accepted
                    message: Tenant-Admin kann erst nach gueltigem Vertragsabschluss eingeladen werden.
                    status: 409
                    request_id: 0bf88f60-aacf-4e3d-8c68-a53d17ff006e
                email_already_in_use:
                  value:
                    code: email_already_in_use
                    message: Diese E-Mail-Adresse ist bereits registriert.
                    status: 409
                    request_id: 5cf8e366-8998-40b9-a7fb-106076fb5acf
  /superadmin/tenants/{tenantSlug}/admin-invitation/resend:
    post:
      tags: [Invitation]
      summary: Tenant-Admin-Einladung neu ausstellen
      operationId: uc00SuperadminResendTenantAdminInvitation
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-40]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '201':
          description: Vorherige offene Einladung widerrufen und neue 72-Stunden-Einladung erzeugt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationResult'
        '409':
          $ref: '#/components/responses/ContractNotAccepted'

  /superadmin/users/{userId}/reset-2fa:
    post:
      tags: [Auth]
      summary: Zwei-Faktor-Authentifizierung kontrolliert zuruecksetzen
      operationId: uc00SuperadminResetTwoFactor
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-41]
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason_code]
              properties:
                reason_code:
                  type: string
                  minLength: 1
                  maxLength: 100
      responses:
        '204':
          description: Zweiter Faktor und aktive Sitzungen widerrufen; Benachrichtigung angestossen
        '404':
          $ref: '#/components/responses/NotFound'

  /tenants/profile:
    get:
      tags: [Tenant Profile]
      summary: Eigenes Schulprofil abrufen
      operationId: uc00GetTenantProfile
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-42]
      responses:
        '200':
          description: Operatives Schulprofil
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags: [Tenant Profile]
      summary: Eigenes Schulprofil speichern
      operationId: uc00UpdateTenantProfile
      security:
        - BearerAuth: []
      x-uc00-rules: [G46, G47, G54, G56]
      x-test-cases: [API-UC00-43, API-UC00-44]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantProfileUpdate'
      responses:
        '200':
          description: Profil gespeichert; rechtliche Vertragsdaten unveraendert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantProfile'
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'

  /onboarding/status:
    get:
      tags: [Onboarding]
      summary: Onboardingstatus abrufen
      operationId: uc00GetOnboardingStatus
      security:
        - BearerAuth: []
      x-uc00-rules: [G48, G50, G51, G52]
      x-test-cases: [API-UC00-45]
      responses:
        '200':
          description: Persistenter Vier-Schritt-Status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'
        '403':
          $ref: '#/components/responses/Forbidden'

  /onboarding/steps/{stepKey}:
    put:
      tags: [Onboarding]
      summary: Onboardingschritt als Draft speichern oder abschliessen
      operationId: uc00SaveOnboardingStep
      security:
        - BearerAuth: []
      x-uc00-rules: [G48, G49, G50, G51, G52, G54, G56]
      x-test-cases: [API-UC00-46, API-UC00-47, API-UC00-48]
      parameters:
        - $ref: '#/components/parameters/StepKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OnboardingStepRequest'
      responses:
        '200':
          description: Schritt gespeichert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStepResult'
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          description: Schritt nicht freigeschaltet oder fremder Tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: E-Mail des ersten Mitarbeiters global belegt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /onboarding/complete:
    post:
      tags: [Onboarding]
      summary: Onboarding idempotent abschliessen
      operationId: uc00CompleteOnboarding
      security:
        - BearerAuth: []
      x-uc00-rules: [G48, G50, G52, G53, G55, G57]
      x-test-cases: [API-UC00-49, API-UC00-50, API-UC00-51]
      responses:
        '200':
          description: Tenant aktiv; wiederholter Aufruf liefert dasselbe Ergebnis
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingCompletion'
        '409':
          description: Pflichtschritt oder Vertragsnachweis fehlt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                onboarding_incomplete:
                  value:
                    code: onboarding_incomplete
                    message: Nicht alle Pflichtschritte sind abgeschlossen.
                    status: 409
                    request_id: 4d41065b-aef2-4f80-9402-7e25b6cb420d
                contract_not_accepted:
                  value:
                    code: contract_not_accepted
                    message: Der Vertragsnachweis ist nicht mehr gueltig.
                    status: 409
                    request_id: 06ed6cde-8537-4d90-a248-6f367b2cfc5d

  /users/invitations:
    post:
      tags: [Users]
      summary: Mitarbeiter tenantbezogen einladen
      operationId: uc00InviteEmployee
      security:
        - BearerAuth: []
      x-uc00-rules: [G1, G36, G41, G50]
      x-test-cases: [API-UC00-52, API-UC00-53]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmployeeInvitationRequest'
      responses:
        '201':
          description: Mitarbeiter im eigenen Tenant eingeladen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationResult'
        '409':
          $ref: '#/components/responses/EmailAlreadyInUse'

  /superadmin/tenants/{tenantSlug}/offboarding:
    get:
      tags: [Offboarding]
      summary: Offboardingstatus abrufen
      operationId: uc00SuperadminGetOffboarding
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-54]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Offboardingstatus und Fristen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OffboardingRecord'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags: [Offboarding]
      summary: Tenant-Offboarding einleiten
      operationId: uc00SuperadminStartOffboarding
      security:
        - BearerAuth: []
      x-uc00-rules: [G99, G107, G108, G109, G110]
      x-test-cases: [API-UC00-55, API-UC00-56]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OffboardingStartRequest'
      responses:
        '201':
          description: Zugriff sofort gesperrt; Export- und Loeschfrist berechnet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OffboardingRecord'
        '409':
          description: Offboarding bereits aktiv oder Status unzulaessig
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /superadmin/tenants/{tenantSlug}/offboarding/cancel:
    post:
      tags: [Offboarding]
      summary: Offboarding vor irreversibler Loeschung abbrechen
      operationId: uc00SuperadminCancelOffboarding
      security:
        - BearerAuth: []
      x-uc00-rules: [G113]
      x-test-cases: [API-UC00-57]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  minLength: 1
                  maxLength: 255
      responses:
        '200':
          description: Tenant kontrolliert reaktiviert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OffboardingRecord'
        '409':
          description: Irreversible Loeschgrenze bereits erreicht
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tenants/exports:
    post:
      tags: [Exports]
      summary: Offboarding-Export anfordern
      operationId: uc00RequestTenantExport
      security:
        - BearerAuth: []
      x-uc00-rules: [G110, G111, G112]
      x-test-cases: [API-UC00-58]
      responses:
        '202':
          description: Exporterzeugung angestossen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantExport'
        '409':
          description: Kein aktiver Exportzugang
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tenants/exports/{exportId}:
    get:
      tags: [Exports]
      summary: Exportstatus abrufen
      operationId: uc00GetTenantExport
      security:
        - BearerAuth: []
      x-test-cases: [API-UC00-59]
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: Exportstatus
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantExport'
        '404':
          $ref: '#/components/responses/NotFound'

  /tenants/exports/{exportId}/download:
    get:
      tags: [Exports]
      summary: Export backendvermittelt herunterladen
      operationId: uc00DownloadTenantExport
      security:
        - BearerAuth: []
      x-uc00-rules: [G105, G111, G112]
      x-test-cases: [API-UC00-60, API-UC00-61]
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: Verschluesseltes Exportarchiv wird durch das Backend gestreamt
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/zip:
              schema:
                type: string
                format: binary
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Exportzugang abgelaufen oder Objekt geloescht
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  headers:
    NoStore:
      description: Vertrauliche Antwort darf nicht zwischengespeichert werden
      schema:
        type: string
        const: no-store

  parameters:
    TenantSlug:
      name: tenantSlug
      in: path
      required: true
      schema:
        type: string
        pattern: '^[a-z0-9]{8}$'
    ContractId:
      name: contractId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    UserId:
      name: userId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ExportId:
      name: exportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    InvitationToken:
      name: token
      in: path
      required: true
      description: Einmaliges Geheimnis; darf nicht protokolliert werden
      schema:
        type: string
        minLength: 32
        maxLength: 512
    ContractToken:
      name: token
      in: path
      required: true
      description: Einmaliges Vertragsgeheimnis; darf nicht protokolliert werden
      schema:
        type: string
        minLength: 32
        maxLength: 512
    StepKey:
      name: stepKey
      in: path
      required: true
      schema:
        type: string
        enum: [school_profile, contact_details, first_employee, confirmation]
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    Offset:
      name: offset
      in: query
      schema:
        type: integer
        minimum: 0
        default: 0

  schemas:
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [code, message, status, request_id]
      properties:
        code:
          type: string
          pattern: '^[a-z0-9_]+$'
        message:
          type: string
        status:
          type: integer
          minimum: 400
          maximum: 599
        request_id:
          type: string
          format: uuid

    ValidationErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          required: [details]
          properties:
            details:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [field, code]
                properties:
                  field:
                    type: string
                  code:
                    type: string
                  message:
                    type: string

    LoginRequest:
      type: object
      additionalProperties: false
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          description: Wird serverseitig getrimmt und kleingeschrieben
        password:
          type: string
          format: password
          minLength: 12
          maxLength: 256

    LoginStartResponse:
      type: object
      additionalProperties: false
      required: [requires_2fa]
      properties:
        requires_2fa:
          type: boolean
        two_factor_session_token:
          type: [string, 'null']
          description: Einmaliges Geheimnis mit hoechstens zehn Minuten Laufzeit
        access_token:
          type: [string, 'null']
        expires_in:
          type: [integer, 'null']
          minimum: 1
        user:
          oneOf:
            - $ref: '#/components/schemas/AuthenticatedUser'
            - type: 'null'

    TwoFactorLoginRequest:
      type: object
      additionalProperties: false
      required: [two_factor_session_token, code]
      properties:
        two_factor_session_token:
          type: string
          minLength: 32
          maxLength: 512
        code:
          type: string
          pattern: '^[0-9]{6}$'

    RecoveryLoginRequest:
      type: object
      additionalProperties: false
      required: [two_factor_session_token, recovery_code]
      properties:
        two_factor_session_token:
          type: string
          minLength: 32
          maxLength: 512
        recovery_code:
          type: string
          minLength: 8
          maxLength: 64

    LoginCompletedResponse:
      type: object
      additionalProperties: false
      required: [access_token, token_type, expires_in, user]
      properties:
        access_token:
          type: string
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          const: 900
        user:
          $ref: '#/components/schemas/AuthenticatedUser'

    AuthenticatedUser:
      type: object
      additionalProperties: false
      required: [id, email, role, status]
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          type: [string, 'null']
          format: uuid
        email:
          type: string
          format: email
        role:
          type: string
          enum: [superadmin, tenant_admin, mitarbeiter, kunde]
        status:
          type: string
          enum: [invited, active, suspended, deactivated]

    InvitationContext:
      type: object
      additionalProperties: false
      required: [email, role, expires_at, requires_2fa_setup]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: [string, 'null']
        last_name:
          type: [string, 'null']
        role:
          type: string
          enum: [tenant_admin, mitarbeiter, kunde]
        tenant_display_name:
          type: string
        expires_at:
          type: string
          format: date-time
        requires_2fa_setup:
          type: boolean
        two_factor_setup_uri:
          type: [string, 'null']
          description: Nur bei erforderlicher 2FA; vertraulich und nicht zu protokollieren

    InvitationAcceptRequest:
      type: object
      additionalProperties: false
      required: [password, password_confirmation]
      properties:
        password:
          type: string
          format: password
          minLength: 12
          maxLength: 256
        password_confirmation:
          type: string
          format: password
          minLength: 12
          maxLength: 256
        totp_code:
          type: [string, 'null']
          pattern: '^[0-9]{6}$'
          description: Pflicht, wenn `requires_2fa_setup` wahr ist

    InvitationAcceptResponse:
      type: object
      additionalProperties: false
      required: [login, recovery_codes]
      properties:
        login:
          $ref: '#/components/schemas/LoginCompletedResponse'
        recovery_codes:
          type: array
          description: Einmalige Anzeige; leer, wenn fuer die Rolle keine 2FA erforderlich ist
          items:
            type: string
          maxItems: 20

    TenantStatus:
      type: string
      enum: [contract_pending, onboarding, active, suspended, offboarding_export, retention_only, deactivated]

    TenantPreparationRequest:
      type: object
      additionalProperties: false
      required: [legal_name, country, plan_type, contract_contact_name, contract_contact_email]
      properties:
        legal_name:
          type: string
          minLength: 2
          maxLength: 255
        country:
          type: string
          enum: [CH, DE, AT]
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]
        contract_contact_name:
          type: string
          minLength: 2
          maxLength: 255
        contract_contact_email:
          type: string
          format: email
          maxLength: 255

    ContractPartyRequest:
      type: object
      additionalProperties: false
      required: [legal_name, street, postal_code, city, country, contract_contact_name, contract_contact_email]
      properties:
        legal_name:
          type: string
          minLength: 2
          maxLength: 255
        street:
          type: string
          minLength: 2
          maxLength: 255
        postal_code:
          type: string
          minLength: 2
          maxLength: 20
        city:
          type: string
          minLength: 2
          maxLength: 100
        country:
          type: string
          enum: [CH, DE, AT]
        vat_number:
          type: [string, 'null']
          maxLength: 50
        contract_contact_name:
          type: string
          minLength: 2
          maxLength: 255
        contract_contact_email:
          type: string
          format: email
          maxLength: 255

    TenantSummary:
      type: object
      additionalProperties: false
      required: [id, slug, legal_name, country, status, plan_type]
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          pattern: '^[a-z0-9]{8}$'
        legal_name:
          type: string
        country:
          type: string
          enum: [CH, DE, AT]
        status:
          $ref: '#/components/schemas/TenantStatus'
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]

    TenantDetail:
      allOf:
        - $ref: '#/components/schemas/TenantSummary'
        - type: object
          required: [contract_party_complete, accepted_contract, tenant_admin]
          properties:
            street:
              type: [string, 'null']
            postal_code:
              type: [string, 'null']
            city:
              type: [string, 'null']
            contract_contact_name:
              type: string
            contract_contact_email:
              type: string
              format: email
            contract_party_complete:
              type: boolean
            accepted_contract:
              oneOf:
                - $ref: '#/components/schemas/ContractRecord'
                - type: 'null'
            tenant_admin:
              oneOf:
                - $ref: '#/components/schemas/AuthenticatedUser'
                - type: 'null'

    ContractRequest:
      type: object
      additionalProperties: false
      required: [contract_version, contract_template_key]
      properties:
        contract_version:
          type: string
          minLength: 1
          maxLength: 50
        contract_template_key:
          type: string
          minLength: 1
          maxLength: 100

    ContractRequestResponse:
      type: object
      additionalProperties: false
      required: [contract, link_expires_at, delivery_status]
      properties:
        contract:
          $ref: '#/components/schemas/ContractRecord'
        link_expires_at:
          type: string
          format: date-time
        delivery_status:
          type: string
          enum: [queued]

    ContractStatus:
      type: string
      enum: [pending_acceptance, pending_review, accepted, rejected, superseded, terminated]

    ContractRecord:
      type: object
      additionalProperties: false
      required: [id, tenant_id, status, contract_version, party_snapshot, created_at]
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/ContractStatus'
        contract_version:
          type: string
        conclusion_type:
          type: [string, 'null']
          enum: [electronic_acceptance, signed_document, null]
        concluded_at:
          type: [string, 'null']
          format: date-time
        supersedes_contract_id:
          type: [string, 'null']
          format: uuid
        party_snapshot:
          $ref: '#/components/schemas/ContractPartyRequest'
        signatory_name:
          type: [string, 'null']
        signatory_email:
          type: [string, 'null']
          format: email
        signatory_function:
          type: [string, 'null']
        document:
          oneOf:
            - $ref: '#/components/schemas/ContractDocument'
            - type: 'null'
        created_at:
          type: string
          format: date-time

    PublicContract:
      type: object
      additionalProperties: false
      required: [contract_version, party, contract_html, expires_at]
      properties:
        contract_version:
          type: string
        party:
          $ref: '#/components/schemas/ContractPartyRequest'
        contract_html:
          type: string
          description: Serverseitig kontrolliert gerenderter Vertragstext ohne aktive Inhalte
        expires_at:
          type: string
          format: date-time

    ContractAcceptanceRequest:
      type: object
      additionalProperties: false
      required: [signatory_name, signatory_email, signatory_function, authority_confirmed]
      properties:
        signatory_name:
          type: string
          minLength: 2
          maxLength: 255
        signatory_email:
          type: string
          format: email
          maxLength: 255
        signatory_function:
          type: string
          minLength: 2
          maxLength: 255
        authority_confirmed:
          type: boolean
          const: true

    ContractUploadRequest:
      type: object
      additionalProperties: false
      required: [file, contract_version, contract_template_key, signatory_name, signatory_email, signatory_function, authority_confirmed]
      properties:
        file:
          type: string
          format: binary
          description: Ausschliesslich PDF bis 10 MiB
        contract_version:
          type: string
          maxLength: 50
        contract_template_key:
          type: string
          maxLength: 100
        signatory_name:
          type: string
          maxLength: 255
        signatory_email:
          type: string
          format: email
          maxLength: 255
        signatory_function:
          type: string
          maxLength: 255
        authority_confirmed:
          type: boolean
          const: true

    ContractDocument:
      type: object
      additionalProperties: false
      required: [id, status, review_status, original_file_name, mime_type, file_size_bytes, document_hash, hash_algorithm]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [quarantined, scanning, clean, rejected, deleted]
        review_status:
          type: string
          enum: [not_required, pending, accepted, rejected]
        original_file_name:
          type: string
        mime_type:
          type: string
          const: application/pdf
        file_size_bytes:
          type: integer
          minimum: 1
          maximum: 10485760
        document_hash:
          type: string
          pattern: '^[0-9A-Fa-f]{64}$'
        hash_algorithm:
          type: string
          const: SHA-256

    ContractReviewRequest:
      type: object
      additionalProperties: false
      required: [decision, reason_code]
      properties:
        decision:
          type: string
          enum: [accept, reject]
        reason_code:
          type: string
          minLength: 1
          maxLength: 100

    AdminInvitationRequest:
      type: object
      additionalProperties: false
      required: [email, first_name, last_name]
      properties:
        email:
          type: string
          format: email
          maxLength: 255
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100

    EmployeeInvitationRequest:
      type: object
      additionalProperties: false
      required: [email, first_name, last_name, role]
      properties:
        email:
          type: string
          format: email
          maxLength: 255
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        role:
          type: string
          const: mitarbeiter

    InvitationResult:
      type: object
      additionalProperties: false
      required: [invitation_id, user_id, status, expires_at, delivery_status]
      properties:
        invitation_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        status:
          type: string
          const: pending
        expires_at:
          type: string
          format: date-time
        delivery_status:
          type: string
          enum: [queued]

    TenantProfile:
      type: object
      additionalProperties: false
      required: [tenant_id, default_currency, contact_email_public, contact_phone_fixed_public, contact_phone_mobile_public, website_public, profile_confirmed]
      properties:
        tenant_id:
          type: string
          format: uuid
        display_name:
          type: [string, 'null']
          maxLength: 255
        address:
          type: [string, 'null']
          maxLength: 255
        postal_code:
          type: [string, 'null']
          maxLength: 20
        city:
          type: [string, 'null']
          maxLength: 100
        country:
          type: [string, 'null']
          enum: [CH, DE, AT, null]
        contact_person:
          type: [string, 'null']
          maxLength: 255
        contact_email:
          type: [string, 'null']
          format: email
        contact_phone_fixed:
          type: [string, 'null']
          maxLength: 50
        contact_phone_mobile:
          type: [string, 'null']
          maxLength: 50
        website:
          type: [string, 'null']
          format: uri
        default_currency:
          type: string
          enum: [CHF, EUR]
        contact_email_public:
          type: boolean
          default: false
        contact_phone_fixed_public:
          type: boolean
          default: false
        contact_phone_mobile_public:
          type: boolean
          default: false
        website_public:
          type: boolean
          default: false
        profile_confirmed:
          type: boolean
        profile_confirmed_at:
          type: [string, 'null']
          format: date-time
        profile_confirmed_by:
          type: [string, 'null']
          format: uuid

    TenantProfileUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        display_name:
          type: [string, 'null']
          maxLength: 255
        address:
          type: [string, 'null']
          maxLength: 255
        postal_code:
          type: [string, 'null']
          maxLength: 20
        city:
          type: [string, 'null']
          maxLength: 100
        country:
          type: [string, 'null']
          enum: [CH, DE, AT, null]
        contact_person:
          type: [string, 'null']
          maxLength: 255
        contact_email:
          type: [string, 'null']
          format: email
        contact_phone_fixed:
          type: [string, 'null']
          maxLength: 50
        contact_phone_mobile:
          type: [string, 'null']
          maxLength: 50
        website:
          type: [string, 'null']
          format: uri
        contact_email_public:
          type: boolean
        contact_phone_fixed_public:
          type: boolean
        contact_phone_mobile_public:
          type: boolean
        website_public:
          type: boolean

    OnboardingStatus:
      type: object
      additionalProperties: false
      required: [tenant_id, status, steps]
      properties:
        tenant_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, in_progress, completed]
        completed_at:
          type: [string, 'null']
          format: date-time
        steps:
          type: array
          minItems: 4
          maxItems: 4
          items:
            $ref: '#/components/schemas/OnboardingStepResult'

    OnboardingStepRequest:
      type: object
      additionalProperties: false
      required: [action, step_data]
      properties:
        action:
          type: string
          enum: [draft, complete, skip]
          description: "`skip` ist ausschliesslich fuer `first_employee` zulaessig"
        step_data:
          oneOf:
            - $ref: '#/components/schemas/SchoolProfileStepData'
            - $ref: '#/components/schemas/ContactDetailsStepData'
            - $ref: '#/components/schemas/FirstEmployeeStepData'
            - $ref: '#/components/schemas/ConfirmationStepData'

    SchoolProfileStepData:
      type: object
      additionalProperties: false
      properties:
        display_name:
          type: [string, 'null']
          maxLength: 255
        address:
          type: [string, 'null']
          maxLength: 255
        postal_code:
          type: [string, 'null']
          maxLength: 20
        city:
          type: [string, 'null']
          maxLength: 100
        country:
          type: [string, 'null']
          enum: [CH, DE, AT, null]
        default_currency:
          type: string
          enum: [CHF, EUR]

    ContactDetailsStepData:
      type: object
      additionalProperties: false
      properties:
        contact_person:
          type: [string, 'null']
          maxLength: 255
        contact_email:
          type: [string, 'null']
          format: email
        contact_phone_fixed:
          type: [string, 'null']
          maxLength: 50
        contact_phone_mobile:
          type: [string, 'null']
          maxLength: 50
        website:
          type: [string, 'null']
          format: uri
        contact_email_public:
          type: boolean
          default: false
        contact_phone_fixed_public:
          type: boolean
          default: false
        contact_phone_mobile_public:
          type: boolean
          default: false
        website_public:
          type: boolean
          default: false

    FirstEmployeeStepData:
      type: object
      additionalProperties: false
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        role:
          type: string
          const: mitarbeiter

    ConfirmationStepData:
      type: object
      additionalProperties: false
      required: [profile_confirmed]
      properties:
        profile_confirmed:
          type: boolean
          const: true

    OnboardingStepResult:
      type: object
      additionalProperties: false
      required: [step_key, status]
      properties:
        step_key:
          type: string
          enum: [school_profile, contact_details, first_employee, confirmation]
        status:
          type: string
          enum: [not_started, in_progress, completed, skipped]
        started_at:
          type: [string, 'null']
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time

    OnboardingCompletion:
      type: object
      additionalProperties: false
      required: [tenant_id, tenant_status, onboarding_status, completed_at]
      properties:
        tenant_id:
          type: string
          format: uuid
        tenant_status:
          type: string
          const: active
        onboarding_status:
          type: string
          const: completed
        completed_at:
          type: string
          format: date-time

    OffboardingStartRequest:
      type: object
      additionalProperties: false
      required: [reason_code, contract_ended_at]
      properties:
        reason_code:
          type: string
          minLength: 1
          maxLength: 100
        contract_ended_at:
          type: string
          format: date-time

    OffboardingRecord:
      type: object
      additionalProperties: false
      required: [id, tenant_id, status, contract_ended_at, export_access_until, operational_delete_by]
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [scheduled, export_access, retention_only, completed, cancelled]
        contract_ended_at:
          type: string
          format: date-time
        export_access_until:
          type: string
          format: date-time
        operational_delete_by:
          type: string
          format: date-time
        deletion_completed_at:
          type: [string, 'null']
          format: date-time

    TenantExport:
      type: object
      additionalProperties: false
      required: [id, status, requested_at]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, generating, ready, failed, expired, deleted]
        requested_at:
          type: string
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        expires_at:
          type: [string, 'null']
          format: date-time
        manifest_hash:
          type: [string, 'null']
          pattern: '^[0-9A-Fa-f]{64}$'
        hash_algorithm:
          type: [string, 'null']
          enum: [SHA-256, null]
        download_count:
          type: integer
          minimum: 0

  responses:
    Unauthorized:
      description: Nicht authentisiert
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: unauthorized
            message: Authentisierung erforderlich.
            status: 401
            request_id: e03176ea-7642-45cd-9c39-c74629043a84
    Forbidden:
      description: Nicht autorisiert
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: forbidden
            message: Zugriff verweigert.
            status: 403
            request_id: 940c8b52-8c9f-4341-8152-27bb11683818
    NotFound:
      description: Nicht gefunden oder wegen Tenant-Isolation nicht sichtbar
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: not_found
            message: Ressource nicht gefunden.
            status: 404
            request_id: 71861f7b-a370-4659-a5e4-fe44a1228ac7
    ValidationError:
      description: Eingabe ungueltig
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
    InvalidCredentials:
      description: Anmeldedaten ungueltig
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: invalid_credentials
            message: E-Mail oder Passwort ist ungueltig.
            status: 401
            request_id: da7169ac-f566-4ca1-a1f8-c709d33a87bc
    AccountLocked:
      description: Konto voruebergehend gesperrt
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimitExceeded:
      description: Ratenbegrenzung erreicht
      headers:
        Retry-After:
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InvalidTwoFactorSession:
      description: Zwei-Faktor-Sitzung ungueltig, abgelaufen, verwendet oder gesperrt
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InvalidTwoFactorCode:
      description: TOTP ungueltig
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    LoginCompleted:
      description: Anmeldung abgeschlossen
      headers:
        Cache-Control:
          $ref: '#/components/headers/NoStore'
        Set-Cookie:
          description: Sicherer HttpOnly-Refresh-Cookie
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/LoginCompletedResponse'
    InvitationGone:
      description: Einladung ungueltig, abgelaufen, widerrufen oder bereits verwendet
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ContractLinkGone:
      description: Vertragslink ungueltig, abgelaufen, widerrufen oder bereits verwendet
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ContractPartyIncomplete:
      description: Vertragspartei oder Vertragsanschrift unvollstaendig
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: contract_party_incomplete
            message: Vollstaendige Vertragspartei und Vertragsanschrift sind erforderlich.
            status: 409
            request_id: 3480c758-801a-4e96-b1dc-475fb38dfb87
    ContractAlreadyAccepted:
      description: Akzeptierter Vertrag ist unveraenderlich
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ContractNotAccepted:
      description: Kein gueltiger akzeptierter Vertrag
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: contract_not_accepted
            message: Gueltiger Vertragsabschluss erforderlich.
            status: 409
            request_id: 34b763af-4e66-476a-89e1-0e027565959f
    EmailAlreadyInUse:
      description: Normalisierte E-Mail global vergeben
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: email_already_in_use
            message: Diese E-Mail-Adresse ist bereits registriert.
            status: 409
            request_id: c70cd66f-890a-4a03-91a8-a37cbe89b0a0
    FileTooLarge:
      description: Datei groesser als 10 MiB
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnsupportedPdf:
      description: Datei ist kein zulaessiges, unverschluesseltes und strukturell gueltiges PDF
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
