# Öffentliche Fassung, erzeugt aus docs/api/openapi.yaml (npm run openapi:oeffentlich). Nicht von Hand ändern.
openapi: 3.1.0
info:
  title: Time Momentum REST-API
  version: 1.1.0
  description: |-
    Öffentliche REST-API von Time Momentum für Entwickler, Skripte und KI-Agenten. Die
    Kommandozeile `tm` deckt jede hier beschriebene Operation ab (https://www.time-momentum.de/entwickler).


    **Anmeldung.** Ein API-Key (`tmk_…`, angelegt in der Web-App unter Datenquellen) gilt direkt als
    Bearer-Token auf den Lese-Routen und den Timern. Einen Key mit Schreibrecht tauscht
    `POST /auth/token` gegen ein Access-Token, das eine Stunde gilt; damit gilt jede Route wie in der
    Web-App. Der Header `X-TM-Workspace` wählt den Workspace.


    **Antwortformat.** Erfolg `{ data }`, Listen mit Cursor-Pagination `{ data, next_cursor }`.
    Fehler immer `{ error: { code, message, details? } }`; `details.field` nennt bei
    Validierungsfehlern (422) das betroffene Feld.


    **Rollen.** Schreibende Operationen verlangen die Rolle `owner` oder `member`; `viewer` →
    403 `forbidden`.


    **Tarif.** Maschinenzugriff ist Teil von Pro: Ohne Pro erreichen API-Keys nur `GET /projects` und
    die Timer, der Token-Tausch antwortet mit 402 `upgrade_required`.
  contact:
    url: https://www.time-momentum.de/entwickler
servers:
  - url: https://app.time-momentum.com/api/v1
    description: Produktion
security:
  - cookieAuth: []
  - bearerJwt: []
tags:
  - name: auth
    description: Token-Tausch für CLI und Agenten (Schreib-API-Key → kurzlebiges Access-Token).
  - name: workspace
    description: Aktiver Workspace, Mitgliedschaften und Workspace-Einstellungen.
  - name: profile
    description: Eigenes Nutzerprofil (Anzeigename, Profilfoto) und Kontolöschung.
  - name: billing
    description: Tarif und Abo (Stripe) — der Tarif gilt je Nutzer.
  - name: api-keys
    description: Verwaltung der eigenen API-Keys (Klartext nur einmalig bei der Erstellung).
  - name: projects
    description: Projekte inklusive Stundensatz, Kundenzuordnung, Abrechnungsregeln und Aktivitäten.
  - name: progress
    description: Projektfortschritt — Wochenpunkte, die eine KI aus Commits, Sitzungstiteln und Zeiteinträgen verdichtet.
  - name: folders
    description: Ordner→Projekt-Zuordnungen, über die der Kollektor KI-Sessions und Commits zuordnet.
  - name: timers
    description: Manuelle Zeiterfassung — Timer starten, pausieren, fortsetzen, stoppen, Zeit nachtragen und korrigieren.
  - name: customers
    description: Kunden mit Stammdaten, Rechnungsempfängern und Abrechnungsregeln.
  - name: costs
    description: |
      Projektkosten: Kostenarten je Projekt (KI-Kosten zu API-Preisen oder anteilig aus Abos, KI-Agentenzeit, Arbeitszeit), KI-Abos des Workspace und Einzelkosten, die sich auf Rechnungen weiterbelasten lassen.
  - name: orders
    description: |
      Aufträge (Beauftragung): kaufmännische Zusage eines Kunden — optional auf ein Projekt eingegrenzt — mit Umsatzvolumen und/oder Zeitkontingent ab dem Beauftragungsdatum. Die geleistete Arbeit wird aus Umfang und Zeitfenster ABGELEITET, das Abgerechnete kommt aus verknüpften Rechnungen.
  - name: ai-sessions
    description: Hook-erfasste KI-Sessions inkl. lernender Projektzuordnung.
  - name: git-commits
    description: Als Kontextmarker erfasste Git-Commits.
  - name: reporting
    description: Auswertungen — Dashboard, Umsatz und Rentabilität, Datei-Export.
  - name: models
    description: Modellpreise (global) und Modell-Aliase (Zuordnung roher Modellnamen).
  - name: invoices
    description: |
      Rechnungen: aus den erfassten Leistungen eines Zeitraums zusammengestellte Belege mit Positionen, Bausteinen, Leistungsnachweisen, Zahlungen, Storno und Export als PDF/Excel/CSV.
  - name: payments
    description: Zahlungseingänge je Rechnung (Teil- und Restzahlungen) und workspaceweite Zahlungsliste.
  - name: statements
    description: |
      Nachweise: eigenständige Dokumente für einen Kunden — Leistungsnachweis (`proof`) oder Fortschrittsbericht (`progress`). Sie belegen die geleistete Arbeit eines Zeitraums, werden festgeschrieben (eingefrorener Snapshot) und ausschließlich als Datei geteilt.
  - name: invoice-design
    description: |
      Rechnungsdesign — die Rechnungsvorlage (Nummernkreis, Texte, Bausteine, Layout, dazu der Bereich Leistungsnachweis mit eigenem Nummernkreis, Texten und Layout) und eigene Layouts je Dokumentart (HTML mit Liquid-Platzhaltern plus CSS).
  - name: branding
    description: Corporate-Design des Workspaces (Firmenname, Akzentfarbe, Logo, Fußzeile) für Exporte.
  - name: share
    description: Öffentliche Share-Karte (Social-Stats) — Konfiguration je Nutzer.
  - name: support
    description: Support-Anfragen und Feedback samt Nachrichtenverlauf.
  - name: github
    description: |
      GitHub-Integration (GitHub App, strikt lesend): Accounts/Orgs verbinden, Repositories mit Projekten verknüpfen und Commits synchronisieren.
  - name: downloads
    description: Auslieferung von Kollektor und Stream-Deck-Plugin.
  - name: limits
    description: |
      Abo-Limits der KI-Werkzeuge (Claude 5-Stunden-/Wochenfenster, Codex-Fenster) — der Kollektor meldet den letzten Stand, Clients wie das Stream Deck lesen ihn.
paths:
  /auth/token:
    post:
      tags:
        - auth
      operationId: exchangeApiKeyToken
      summary: Schreib-API-Key gegen ein kurzlebiges Access-Token tauschen
      description: "Tauscht einen API-Key mit Berechtigung `write` gegen ein kurzlebiges Supabase-Access-Token des Key-Inhabers (`bearerJwt`). Damit laufen alle übrigen Routen über den normalen Session-Pfad — RLS, Rollen, RPCs und Trigger wie in der Web-App. Es wird KEIN Refresh-Token ausgegeben: nach Ablauf (Standard 1 h) oder bei 401 erneut tauschen. Je Key lebt höchstens eine Sitzung (ein neuer Tausch beendet die vorige); ein Widerruf des Keys beendet sie sofort für diese API. Grenze: Supabase selbst (PostgREST, Storage) prüft nur Signatur und Ablauf des Tokens — dort bleibt ein schon ausgestelltes Token bis zum Ablauf gültig, sensible Kontoaktionen sind für Tausch-Sitzungen aber serverseitig gesperrt (siehe `POST /api-keys`, `DELETE /account`; Passwortänderung verlangt eine Reauthentifizierung per E-Mail). Es wird keine E-Mail versendet. Das Rate-Limit des Keys zählt mit; die RateLimit-Header stehen in jeder Antwort. Zusätzlich gilt ein eigenes Tausch-Limit (5 Tausche je Key und 10 je Nutzer in 5 Minuten) → 429 mit `Retry-After`. Die Antwort trägt `Cache-Control: no-store`."
      security:
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Token ausgestellt.
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/AuthToken"
        "401":
          description: API-Key fehlt oder ist ungültig (`unauthorized`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: Key ist nur lesend („Key ist nur lesend“), widerrufen bzw. nicht (mehr) berechtigt, oder der Key-Inhaber kann sich nicht anmelden (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: Sitzung konnte nicht erzeugt oder gebunden werden (`internal_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /workspace:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - workspace
      operationId: getCurrentWorkspace
      summary: Aktiven Workspace, eigene Rolle und Zugangsweg lesen
      description: "Grundlage für `tm whoami` / `tm workspace show`. Auth: Session, Bearer-JWT ODER API-Key (lesend, rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Aktiver Workspace.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/CurrentWorkspace"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
  /workspaces:
    get:
      tags:
        - workspace
      operationId: listWorkspaces
      summary: Eigene Workspace-Mitgliedschaften auflisten
      description: Alle nicht gelöschten Workspaces, in denen der Nutzer Mitglied ist (älteste zuerst). `active` markiert den Workspace, gegen den die übrigen Endpunkte gerade arbeiten — per `X-TM-Workspace` umschaltbar.
      parameters:
        - $ref: "#/components/parameters/WorkspaceHeader"
      responses:
        "200":
          description: Mitgliedschaften.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WorkspaceMembership"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /workspace/settings:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - workspace
      operationId: getWorkspaceSettings
      summary: Account-weite Einstellungen lesen
      description: "Liefert die für den ganzen Account geltenden Einstellungen: `invoice_retention_mode` — wie mit gestellten Rechnungen umgegangen wird (ohne gesetzten oder bei unbekanntem Wert gilt der strengere Standard `audit`) — und `ai_features`, die Schalter der KI-Funktionen samt Zustimmungsnachweis (Standard: alle aus). Alle Rollen."
      responses:
        "200":
          description: Aktuelle Einstellungen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/WorkspaceSettings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    put:
      tags:
        - workspace
      operationId: updateWorkspaceSettings
      summary: Account-weite Einstellungen speichern (nur Owner)
      description: "Teil-Update: Nur die mitgeschickten Felder ändern sich, mindestens eines ist Pflicht. `invoice_retention_mode` setzt den Aufbewahrungsmodus der Rechnungen. `audit` (Standard): eine festgeschriebene Rechnung wird nie gelöscht, Korrekturen laufen über `POST /invoices/{id}/cancel`. `delete`: festgeschriebene Rechnungen dürfen über `DELETE /invoices/{id}?hard=true` endgültig gelöscht werden. Entwürfe sind in beiden Modi löschbar. `ai_features` schaltet KI-Funktionen ein oder aus, die Workspace-Daten an den KI-Anbieter (Anthropic) geben. Das Einschalten gilt als Zustimmung und wird mit Zeitpunkt und Person festgehalten. Nur der Owner (member/viewer → 403)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                invoice_retention_mode:
                  type: string
                  enum:
                    - audit
                    - delete
                ai_features:
                  type: object
                  additionalProperties: false
                  minProperties: 1
                  properties:
                    progress:
                      type: boolean
                      description: Projektfortschritt aus Commits, Sitzungstiteln und Zeiteinträgen.
                    session_titles:
                      type: boolean
                      description: "KI-Kurztitel aus dem Anfang des ersten Prompts für Sitzungen, deren Werkzeug keinen Titel führt. Aus: der Prompt wird verworfen, die Sitzung bleibt ohne Titel."
      responses:
        "200":
          description: Einstellungen gespeichert.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/WorkspaceSettings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /profile:
    get:
      tags:
        - profile
      operationId: getProfile
      summary: Eigenes Profil lesen
      responses:
        "200":
          description: Profilzeile des angemeldeten Nutzers.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Profile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags:
        - profile
      operationId: updateProfile
      summary: Anzeigenamen ändern
      description: Der Anzeigename erscheint in der Sidebar und belegt den Namen der Share-Karte vor.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - full_name
              properties:
                full_name:
                  type: string
                  minLength: 1
                  maxLength: 80
                  description: Wird getrimmt; leer → 422.
      responses:
        "200":
          description: Aktualisiertes Profil.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Profile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
  /profile/avatar:
    get:
      tags:
        - profile
      operationId: getProfileAvatar
      summary: Eigenes Profilfoto herunterladen
      parameters:
        - name: size
          in: query
          required: false
          schema:
            type: string
            enum:
              - small
              - full
            default: small
          description: "`small` = 256 px, `full` = Original (1024 px). Anderer Wert → 422."
      responses:
        "200":
          description: Bild-Bytes (privat cachebar).
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - profile
      operationId: uploadProfileAvatar
      summary: Profilfoto hochladen (zwei Auflösungen)
      description: "Multipart-Upload mit zwei bereits zugeschnittenen Bildern: `full` (Original, höchstens 2 MB) und `small` (kleine Variante, höchstens 512 KB), jeweils JPEG oder PNG. Ersetzt ein vorhandenes Foto."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - full
                - small
              properties:
                full:
                  type: string
                  format: binary
                  description: JPEG/PNG, ≤ 2.097.152 Bytes.
                small:
                  type: string
                  format: binary
                  description: JPEG/PNG, ≤ 524.288 Bytes.
            encoding:
              full:
                contentType: image/jpeg, image/png
              small:
                contentType: image/jpeg, image/png
      responses:
        "200":
          description: Profil mit den neuen Bildpfaden.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Profile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          description: Upload in den Speicher fehlgeschlagen (`internal_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags:
        - profile
      operationId: deleteProfileAvatar
      summary: Profilfoto entfernen
      responses:
        "200":
          description: Profil ohne Bildpfade.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Profile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /account:
    delete:
      tags:
        - profile
      operationId: deleteOwnAccount
      summary: Eigenes Konto vollständig löschen (Löschrecht Art. 17 DSGVO)
      description: "Löscht das Konto des angemeldeten Nutzers unwiderruflich: Profil, Owner-Workspaces samt aller Fachdaten (per Cascade) und den Auth-Nutzer. Nimmt KEINE ID entgegen. Nicht mit einem Token aus dem Key-Tausch (`POST /auth/token`) möglich → 403 (`details.reason = api_key_session`); nur mit einer echten Anmeldung."
      responses:
        "200":
          description: Konto gelöscht.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - deleted
                    properties:
                      deleted:
                        type: boolean
                        const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          description: Konto konnte nicht gelöscht werden (`internal_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /billing:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - billing
      operationId: getBillingStatus
      summary: Tarif, Abo-Stand und Nutzung gegen die Free-Grenzen
      description: Der Tarif gilt je Nutzer, die Nutzung wird im aktiven Workspace gezählt (aktive Projekte; geloggte KI-Sessions lifetime inkl. gelöschter). `limits_enforced` sagt, ob die Grenzen derzeit erzwungen werden.
      responses:
        "200":
          description: Tarifstand.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/BillingStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
  /billing/checkout:
    post:
      tags:
        - billing
      operationId: createBillingCheckout
      summary: Stripe-Checkout für einen Tarif starten
      description: Liefert die Stripe-Checkout-URL, auf die der Client weiterleitet. `price_id` ist ein Preis aus dem konfigurierten Angebot (Tarifseite `/app/billing`); ohne Angabe gilt der alte feste Pro-Preis, sofern er in dieser Umgebung gesetzt ist. `code` ist ein Rabattcode — geprüft werden Gültigkeit, Einlösezeitraum, Kontingent und Geltungsbereich. Eine bereits offene Checkout-Session mit derselben Auswahl wird wiederverwendet, eine mit anderer Auswahl beendet.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                price_id:
                  type: string
                  format: uuid
                  nullable: true
                  description: Preis aus dem konfigurierten Angebot.
                code:
                  type: string
                  maxLength: 40
                  description: Rabattcode (Groß-/Kleinschreibung egal).
      responses:
        "200":
          description: Checkout-URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UrlResult"
        "400":
          description: Rabattcode nicht einlösbar (`invalid_discount_code`); `details.reason` ist einer von `unbekannt`, `inaktiv`, `noch_nicht_gueltig`, `abgelaufen`, `ausgeschoepft`, `nicht_fuer_preis`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Es besteht bereits ein aktives Abo (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          description: Checkout konnte nicht gestartet werden (`internal_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /billing/portal:
    post:
      tags:
        - billing
      operationId: createBillingPortal
      summary: Stripe-Kundenportal öffnen (Upgrade, Kündigung, Zahlungsdaten)
      responses:
        "200":
          description: Portal-URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UrlResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Kein Stripe-Kunde vorhanden (`no_subscription`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Portal konnte nicht geöffnet werden (`internal_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /api-keys:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - api-keys
      operationId: listApiKeys
      summary: Eigene API-Keys des aktiven Workspace auflisten
      description: Nur eigene, nicht gelöschte Keys (auch widerrufene); nie Hash oder Klartext.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite eigener Keys, neueste zuerst.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - api-keys
      operationId: createApiKey
      summary: API-Key erstellen (Klartext nur einmalig)
      description: "Erzeugt einen nutzergebundenen Key für den aktiven Workspace. `permission` legt fest, was der Key darf, und ist danach nicht mehr änderbar: `read` (Standard) — Daten-API lesen, Kollektor-Ingest und Timer der lokalen Clients; `write` — zusätzlich der Token-Tausch (`POST /auth/token`), der Key handelt dann mit allen Rechten des Nutzers. Mit einem Token aus dem Key-Tausch lassen sich KEINE Keys anlegen → 403 (`details.reason = api_key_session`): ein abgeflossener Key soll sich nicht selbst verewigen können."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                permission:
                  type:
                    - string
                    - "null"
                  enum:
                    - read
                    - write
                    - null
                  default: read
                  description: Fehlt oder null → `read`.
      responses:
        "201":
          description: Key erstellt. `key` (Klartext) wird NUR hier zurückgegeben.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ApiKeyCreated"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /api-keys/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    patch:
      tags:
        - api-keys
      operationId: renameApiKey
      summary: API-Key umbenennen
      description: Nur der Name ist änderbar (reine Wiedererkennung). Ein mitgesendetes `permission` wird mit 422 abgelehnt — für eine andere Berechtigung einen neuen Key erstellen.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
      responses:
        "200":
          description: Key umbenannt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /api-keys/{id}/revoke:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - api-keys
      operationId: revokeApiKey
      summary: API-Key widerrufen
      description: Setzt `revoked_at`; der Key wird sofort abgelehnt. Eine per Token-Tausch gebundene Sitzung eines Schreib-Keys wird dabei beendet, ausgestellte Access-Tokens verlieren ihre Gültigkeit.
      responses:
        "200":
          description: Key widerrufen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /projects:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - projects
      operationId: listProjects
      summary: Projekte auflisten
      description: "Auth: Session, Bearer-JWT ODER API-Key (lesend, rate-limitiert). Neueste zuerst."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - archived
              - all
            default: active
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der Projekte.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Project"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags:
        - projects
      operationId: createProject
      summary: Projekt anlegen
      description: Mit `customer_id` wird der Standard-Stundensatz des Kunden als Kopie übernommen (danach unabhängig änderbar) — nur bei Time & Material; der Kunde muss aktiv im selben Workspace liegen. Ohne `billing_model` wird das Projekt eines Pro-bono-Kunden Pro bono, sonst Time & Material. Projekte sind in jedem Tarif unbegrenzt.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type: string
                  maxLength: 2000
                customer_id:
                  type:
                    - string
                    - "null"
                  format: uuid
                billing_model:
                  $ref: "#/components/schemas/ProjectBillingModel"
                match_keywords:
                  type: array
                  maxItems: 50
                  items:
                    type: string
                    minLength: 2
                    maxLength: 100
                  description: Stichworte für die Zuordnung von KI-Sessions über ihren Titel (Groß-/Kleinschreibung egal; trifft am Wortanfang, „Praxis“ also auch „Praxismonatsabschluss“; es gewinnen die meisten, dann die längeren Treffer). Doppelte werden zusammengefasst.
      responses:
        "201":
          description: Projekt angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /projects/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - projects
      operationId: getProject
      summary: Projekt lesen (auch archivierte)
      responses:
        "200":
          description: Projekt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    patch:
      tags:
        - projects
      operationId: updateProject
      summary: Projekt bearbeiten, archivieren oder wiederherstellen
      description: "Nur übergebene Felder ändern sich. `archived: true` archiviert das Projekt und seine aktiven Ordner-Zuordnungen; `archived: false` stellt es wieder her (Ordner-Zuordnungen bleiben archiviert). Ein Wechsel von `billing_model` weg von Time & Material leert den Stundensatz; zurück zu Time & Material ohne `hourly_rate_cents` übernimmt er den Standardsatz des Kunden. Tragen laufende Aufträge des Projekts eine andere Art (oder hat ein Pro-bono-Projekt einen laufenden Auftrag), antwortet die API mit 409."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectUpdate"
      responses:
        "200":
          description: Aktualisiertes Projekt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - projects
      operationId: deleteProject
      summary: Projekt archivieren (Soft-Delete)
      description: "Archiviert das Projekt samt aktiver Ordner-Zuordnungen, wie PATCH `archived: true`. Daten bleiben erhalten. Bereits archiviert → 404. Wiederherstellen per PATCH `archived: false`."
      responses:
        "200":
          $ref: "#/components/responses/Archived"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /projects/{id}/costs:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - costs
      operationId: getProjectCosts
      summary: Kosten eines Projekts im Zeitfenster
      description: "Kosten je Kostenart nach den Einstellungen des Projekts: KI-Kosten (API-Preise, Abo-Anteil oder keine), KI-Agentenzeit, Arbeitszeit, Einzelkosten (netto, Datum im Zeitraum) und die Summe, dazu die Einstellungen und — im Modus `subscription` — die Abos mit dem Anteil dieses Projekts. Dasselbe Fenster wie `GET /revenue` (`from`/`to` schlagen `range`). Geld in µ€. Auth: Session, Bearer-JWT ODER API-Key."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: range
          in: query
          schema:
            type: string
            enum:
              - 7d
              - 30d
              - month
              - all
            default: 30d
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Kostenaufstellung.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectCostReport"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /projects/{id}/progress:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - progress
      operationId: listProjectProgress
      summary: Projektfortschritt (Wochenpunkte) lesen
      description: "Wochen mit Fortschrittspunkten, Belegzahl und letztem Erzeugungslauf, neueste zuerst. Ohne `from`/`to` die letzten 12 Wochen einschließlich der laufenden; höchstens 53 Wochen. Je Punkt der Aufwand (Arbeitszeit der Zeiteinträge, aktive KI-Zeit der Sitzungen, aus den Belegen gerechnet) und die Belege. `enabled` zeigt den Workspace-Schalter; Lesen geht auch, wenn er aus ist. Auth: Session, Bearer-JWT ODER API-Key."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Wochen mit Punkten.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProgressList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /projects/{id}/progress/generate:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - progress
      operationId: generateProjectProgress
      summary: Wochenpunkte per KI erzeugen bzw. neu erzeugen
      description: Erzeugt jede Woche (Montag–Sonntag, Europe/Berlin) von `from` bis `to` mit Belegen neu — höchstens 12 Wochen, ohne Angaben die letzte abgeschlossene Woche. Ersetzt nur unbearbeitete, sichtbare KI-Punkte; bearbeitete, eigene und ausgeblendete Punkte bleiben, ihre Belege gehen nicht erneut an die KI. Commit-Meldungen, Titel der KI-Sitzungen und Beschreibungen der Zeiteinträge gehen dafür an den KI-Anbieter (Anthropic). Ist der Workspace-Schalter Projektfortschritt aus, antwortet der Endpunkt mit 409 `progress_disabled`. Höchstens 60 Wochen je Workspace und Tag (429). Dauert je Woche 10–60 s.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                from:
                  type: string
                  format: date
                to:
                  type: string
                  format: date
      responses:
        "200":
          description: Ergebnis je Woche.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - weeks
                    properties:
                      weeks:
                        type: array
                        items:
                          type: object
                          required:
                            - week_start
                            - status
                            - points
                            - ref_count
                            - covered_count
                          properties:
                            week_start:
                              type: string
                              format: date
                            generation_id:
                              type:
                                - string
                                - "null"
                              format: uuid
                            status:
                              type: string
                              enum:
                                - done
                                - failed
                                - skipped
                            points:
                              type: integer
                            ref_count:
                              type: integer
                            covered_count:
                              type: integer
                            error:
                              type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /projects/{id}/progress/report:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - progress
      operationId: reportProjectProgress
      summary: Bericht über einen Zeitraum per KI (Verdichtung der Wochenpunkte)
      description: "Verdichtet die sichtbaren Wochenpunkte, deren Datum den Zeitraum berührt, zu einer Zusammenfassung (2–3 Sätze) und etwa 6–12 Punkten mit Aufwand (Stufe 2, Modell Haiku). Bearbeitete und eigene Wochenpunkte fließen ein. Standard: die laufende und die drei Wochen davor. Gibt es zu unveränderten Wochenpunkten schon einen Bericht desselben Zeitraums, kommt er ohne KI-Aufruf zurück (`reused`); `fresh: true` erzwingt einen neuen. Titel und Texte der Wochenpunkte gehen dafür an den KI-Anbieter (Anthropic); bei ausgeschaltetem Workspace-Schalter 409 `progress_disabled`. Höchstens 20 neue Berichte je Workspace und Tag (429); ohne Pro ein Probe-Bericht je Workspace (402, `feature: progress_report`). Ohne Wochenpunkte im Zeitraum 422. Dauert 20–60 s."
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                from:
                  type: string
                  format: date
                to:
                  type: string
                  format: date
                fresh:
                  type: boolean
      responses:
        "200":
          description: Der Bericht.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProgressReport"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /projects/{id}/progress/entries:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - progress
      operationId: createProgressEntry
      summary: Eigenen Fortschrittspunkt anlegen
      description: "Legt einen Punkt ohne KI an (`origin: manual`). Die Woche ergibt sich aus `week_start` oder `happened_from`, sonst aus heute. Die KI fasst eigene Punkte nie an."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProgressEntryInput"
      responses:
        "201":
          description: Angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProgressEntryRow"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /projects/{id}/progress/entries/{entryId}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - name: entryId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - $ref: "#/components/parameters/WorkspaceHeader"
    patch:
      tags:
        - progress
      operationId: updateProgressEntry
      summary: Fortschrittspunkt bearbeiten, ausblenden oder umsortieren
      description: "Nur übergebene Felder ändern sich. Ein inhaltlich bearbeiteter KI-Punkt wird zu `ai_edited` und von „Neu erzeugen“ nicht mehr ersetzt. `hidden: true` blendet aus, statt zu löschen — die KI erzeugt den Punkt dann nicht neu."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProgressEntryPatch"
      responses:
        "200":
          description: Gespeichert.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProgressEntryRow"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - progress
      operationId: deleteProgressEntry
      summary: Fortschrittspunkt löschen
      description: "Löscht den Punkt. Ein gelöschter KI-Punkt kann beim nächsten „Neu erzeugen“ wieder entstehen; dauerhaft weg hält ihn `hidden: true`."
      responses:
        "200":
          description: Gelöscht.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - id
                      - deleted
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
                        const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /projects/{id}/activity:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - projects
      operationId: listProjectActivity
      summary: Aktivitäten-Historie eines Projekts (Sessions, Commits, gestoppte Timer)
      description: Gemergt und absteigend nach effektivem Aktivitäts-Zeitstempel (Session `started_at`, Commit `committed_at` ersatzweise `created_at`, Timer `started_at`), mit Kosten je Session. Dieselbe Quelle wie die Projektseite. Der Cursor ist ein eigener Timeline-Cursor.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der Aktivitäten.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ActivityItem"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /folders:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - folders
      operationId: listFolders
      summary: Ordner-Zuordnungen auflisten (optional je Projekt)
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der aktiven Zuordnungen, neueste zuerst.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Folder"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - folders
      operationId: createFolder
      summary: Ordner einem Projekt zuordnen
      description: Der Kollektor ordnet KI-Sessions und Commits über den längsten passenden Ordnerpfad zu. Abschließende Schrägstriche werden entfernt. Ein Pfad kann nur einem aktiven Projekt gehören (sonst 409).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - project_id
                - folder_path
              properties:
                project_id:
                  type: string
                  format: uuid
                  description: Aktives Projekt im Workspace (sonst 422).
                folder_path:
                  type: string
                  minLength: 1
                  maxLength: 1024
                  description: Absoluter Ordnerpfad oder Repository (siehe Folder.folder_path).
                fallback:
                  type: boolean
                  default: false
                  description: Als Auffangregel anlegen (siehe Folder.fallback).
      responses:
        "201":
          description: Zuordnung angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Folder"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /folders/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    patch:
      tags:
        - folders
      operationId: updateFolder
      summary: Ordner-Zuordnung ändern (Projekt, Pfad oder Auffangregel)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                project_id:
                  type: string
                  format: uuid
                folder_path:
                  type: string
                  minLength: 1
                  maxLength: 1024
                fallback:
                  type: boolean
                  description: Auffangregel an oder aus (siehe Folder.fallback).
      responses:
        "200":
          description: Aktualisierte Zuordnung.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Folder"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - folders
      operationId: deleteFolder
      summary: Ordner-Zuordnung entfernen (Soft-Delete)
      responses:
        "200":
          $ref: "#/components/responses/Deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /timers:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - timers
      operationId: listTimers
      summary: Timer und Zeiteinträge auflisten
      description: "`active` = laufend + pausiert, `stopped` = abgeschlossen, `all` = alle nicht gelöschten. Neueste zuerst (nach Anlage). `active_seconds` ist die servergerechnete aktive Dauer ohne Pausen. Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - stopped
              - all
            default: all
        - name: project_id
          in: query
          required: false
          description: Nur Einträge dieses Projekts. Keine gültige UUID → 422.
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der Timer.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Timer"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags:
        - timers
      operationId: startTimer
      summary: Timer starten oder Zeit nachtragen
      description: "Ohne `ended_at` startet ein laufender Timer. Mit `ended_at` entsteht ein bereits abgeschlossener Zeiteintrag (manueller Nachtrag, Status `stopped`): aktive Dauer = `duration_seconds` oder ohne Angabe das volle Zeitfenster; ein kleinerer Wert bildet Pausen ab. `client_id` macht Wiederholungen idempotent — existiert bereits ein Timer mit dieser ID, kommt er mit 200 unverändert zurück. Auch per API-Key (lokale Clients, workspace-hart aus dem Key, owner/member)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimerCreate"
      responses:
        "200":
          description: Idempotente Wiederholung — der bestehende Timer mit dieser `client_id`.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "201":
          description: Timer gestartet bzw. Zeiteintrag nachgetragen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /timers/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - timers
      operationId: getTimer
      summary: Timer lesen
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Timer.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      tags:
        - timers
      operationId: updateTimer
      summary: Zeiteintrag korrigieren
      description: "`description`, `project_id` und `billability` sind in jedem Zustand änderbar; `ended_at` und `duration_seconds` nur bei gestopptem Timer (sonst 409). `started_at` allein ist auch bei laufendem oder pausiertem Timer erlaubt (Rückdatieren): die aktive Dauer verschiebt sich um denselben Betrag, Pausen bleiben erhalten; eine Startzeit in der Zukunft oder nach der bisher erfassten aktiven Zeit ergibt 422. Die effektive aktive Dauer darf das effektive Zeitfenster nie überschreiten (422). Ein Eintrag auf einer gestellten Rechnung oder in einem festgeschriebenen, nicht archivierten Leistungsnachweis ist gesperrt (409, `details.reason` = `billed` bzw. `statement_final`). Auch per API-Key (lokale Clients)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimerUpdate"
      responses:
        "200":
          description: Korrigierter Timer.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Ende oder Dauer an einem laufenden/pausierten Timer, oder der Eintrag steht auf einer gestellten Rechnung bzw. in einem festgeschriebenen Leistungsnachweis (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      tags:
        - timers
      operationId: deleteTimer
      summary: Timer bzw. Zeiteintrag löschen (Soft-Delete)
      description: Ein Eintrag auf einer gestellten Rechnung oder in einem festgeschriebenen, nicht archivierten Leistungsnachweis ist gesperrt (409).
      responses:
        "200":
          $ref: "#/components/responses/Deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Der Eintrag steht auf einer gestellten Rechnung oder in einem festgeschriebenen Leistungsnachweis (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /timers/{id}/split:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - timers
      operationId: splitTimer
      summary: Zeiteintrag teilen
      description: "Teilt einen gestoppten, noch nicht abgerechneten Zeiteintrag in zwei. Der ursprüngliche Eintrag behält ID und Beginn und trägt `seconds` aktive Dauer; ein neuer Eintrag mit derselben Beschreibung, demselben Projekt und derselben Abrechenbarkeit trägt den Rest. Die Zeitspanne wird anteilig geschnitten, die Summe der Dauer bleibt gleich. Grundlage der Auftragsbindung: der Grenzeintrag einer Rechnung zum Auftrag wird so geteilt, dass genau der Teil abgerechnet wird, der noch in den Auftrag passt. Auth: Session mit Schreibrolle."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - seconds
              properties:
                seconds:
                  type: integer
                  minimum: 1
                  description: Aktive Dauer des ersten Teils in Sekunden (kleiner als die ganze Dauer).
      responses:
        "200":
          description: Beide Einträge nach dem Teilen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - first
                      - second
                    properties:
                      first:
                        $ref: "#/components/schemas/Timer"
                      second:
                        $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Eintrag läuft noch, ist bereits abgerechnet, steht in einem festgeschriebenen Leistungsnachweis oder wurde zwischenzeitlich geändert (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /timers/{id}/stop:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - timers
      operationId: stopTimer
      summary: Timer stoppen
      description: Zustandswechsel mit der Server-Uhr, race-sicher (bedingtes Update auf den gelesenen Stand). Bereits gestoppt → 409. Auch per API-Key (lokale Clients, workspace-hart aus dem Key).
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Timer nach dem Wechsel.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Ungültiger Zustandswechsel oder Timer zwischenzeitlich geändert (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /timers/{id}/pause:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - timers
      operationId: pauseTimer
      summary: Timer pausieren
      description: Zustandswechsel mit der Server-Uhr, race-sicher (bedingtes Update auf den gelesenen Stand). Nur ein laufender Timer lässt sich pausieren (sonst 409). Auch per API-Key (lokale Clients, workspace-hart aus dem Key).
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Timer nach dem Wechsel.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Ungültiger Zustandswechsel oder Timer zwischenzeitlich geändert (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /timers/{id}/resume:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - timers
      operationId: resumeTimer
      summary: Timer fortsetzen
      description: Zustandswechsel mit der Server-Uhr, race-sicher (bedingtes Update auf den gelesenen Stand). Nur ein pausierter Timer lässt sich fortsetzen (sonst 409). Auch per API-Key (lokale Clients, workspace-hart aus dem Key).
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Timer nach dem Wechsel.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Timer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Ungültiger Zustandswechsel oder Timer zwischenzeitlich geändert (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /timers/{id}/match-suggestions:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - timers
      operationId: timerMatchSuggestions
      summary: Beschreibungsvorschlag aus überlappenden KI-Sessions
      description: Findet zeitlich überlappende, projekt-kompatible KI-Sessions (höchstens 200 Kandidaten) und leitet einen Freitext-Vorschlag ab. Übernehmen = `suggestion` per PATCH /timers/{id} als `description` setzen.
      responses:
        "200":
          description: Vorschlag und gerankte Treffer.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - suggestion
                      - matches
                    properties:
                      suggestion:
                        type:
                          - string
                          - "null"
                      matches:
                        type: array
                        items:
                          $ref: "#/components/schemas/MatchSuggestion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /customers:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - customers
      operationId: listCustomers
      summary: Kunden auflisten
      description: "Neueste zuerst. Auth: Session, Bearer-JWT ODER API-Key (lesend, rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - archived
              - all
            default: active
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der Kunden.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Customer"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags:
        - customers
      operationId: createCustomer
      summary: Kunden anlegen
      description: "`name` ist Pflicht. Ohne `customer_number` vergibt der Server die nächste freie Kundennummer; ohne `country_code` gilt `DE`. `billing_address` wird aus den Anschriftbausteinen zusammengesetzt, sobald einer davon übergeben wird."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/CustomerInput"
                - type: object
                  required:
                    - name
      responses:
        "201":
          description: Kunde angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Customer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /customers/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - customers
      operationId: getCustomer
      summary: Kunden lesen (auch archivierte)
      responses:
        "200":
          description: Kunde.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Customer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    patch:
      tags:
        - customers
      operationId: updateCustomer
      summary: Kunden bearbeiten, archivieren oder wiederherstellen
      description: "Nur übergebene Felder ändern sich. Ein Teil-PATCH der Anschrift ergänzt die übrigen Bausteine aus dem gespeicherten Stand. `archived: true` archiviert, `false` stellt wieder her."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/CustomerInput"
                - type: object
                  properties:
                    archived:
                      type: boolean
      responses:
        "200":
          description: Aktualisierter Kunde.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Customer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - customers
      operationId: deleteCustomer
      summary: Kunden archivieren (Soft-Delete)
      description: Projekte, Zuordnungen und historische Daten bleiben erhalten. Bereits archiviert → 404.
      responses:
        "200":
          $ref: "#/components/responses/Archived"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /ai-subscriptions:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - costs
      operationId: listAiSubscriptions
      summary: KI-Abos des Workspace
      description: Nicht gelöschte Abos, ältester Beginn zuerst. Jedes Abo wird je Monat vollständig nach dem Anteil der Projekte an den Netto-Token seines Anbieters verteilt; Projekte mit `cost_ai_mode = subscription` buchen ihren Anteil als Kosten.
      responses:
        "200":
          description: Abos.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AiSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - costs
      operationId: createAiSubscription
      summary: KI-Abo anlegen
      description: "Monate werden auf den Monatsersten gelegt. Auth: Session mit Schreibrolle."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/AiSubscriptionInput"
                - required:
                    - name
                    - provider
                    - monthly_amount_cents
                    - valid_from
      responses:
        "201":
          description: Angelegtes Abo.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/AiSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
  /ai-subscriptions/{id}/allocation:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - costs
      operationId: getAiSubscriptionAllocation
      summary: Verteilung eines KI-Abos in einem Monat
      description: "Der volle Monatsbetrag, verteilt nach dem Anteil der Projekte an den Netto-Token des Anbieters (erkannt am Modell der Session; Sessions ohne Projekt zählen nicht). Im laufenden Monat über die Tokens bis heute. Je Projekt Token, Anteil und `cost_ai_mode` — als Kosten bucht den Anteil nur ein Projekt im Modus `subscription`. Geld in µ€. Auth: Session, Bearer-JWT ODER API-Key."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: month
          in: query
          description: Monat als `YYYY-MM`; ohne Angabe der laufende Monat.
          schema:
            type: string
            pattern: ^\d{4}-(0[1-9]|1[0-2])$
      responses:
        "200":
          description: Verteilung.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      subscription:
                        $ref: "#/components/schemas/AiSubscription"
                      month:
                        type: string
                        format: date
                        description: Monatserster.
                      allocated_micros:
                        type: integer
                      total_tokens:
                        type: integer
                      projects:
                        type: array
                        items:
                          type: object
                          properties:
                            project_id:
                              type: string
                              format: uuid
                            name:
                              type: string
                            tokens:
                              type: integer
                            share_micros:
                              type: integer
                            cost_ai_mode:
                              type: string
                              enum:
                                - none
                                - api
                                - subscription
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /ai-subscriptions/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    patch:
      tags:
        - costs
      operationId: updateAiSubscription
      summary: KI-Abo ändern
      description: PATCH-Semantik, mindestens ein Feld.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AiSubscriptionInput"
      responses:
        "200":
          description: Geändertes Abo.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/AiSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - costs
      operationId: deleteAiSubscription
      summary: KI-Abo entfernen
      description: Soft-Delete — das Abo zählt danach in keinem Zeitraum mehr. Wer ein beendetes Abo historisch behalten will, setzt stattdessen `valid_to`.
      responses:
        "200":
          description: Entfernt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /project-expenses:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - costs
      operationId: listProjectExpenses
      summary: Einzelkosten auflisten
      description: Neueste zuerst, je Zeile mit `billed_on` (Rechnung der Weiterbelastung) oder null.
      parameters:
        - name: project_id
          in: query
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Einzelkosten.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ProjectExpense"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - costs
      operationId: createProjectExpense
      summary: Einzelkosten erfassen
      description: "Fehlt `vat_amount_cents`, wird er aus Netto × Satz vorgeschlagen; ohne gezahlte Mehrwertsteuer ist er 0. Auth: Session mit Schreibrolle."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/ProjectExpenseInput"
                - type: object
                  required:
                    - project_id
                    - title
                    - amount_net_cents
                  properties:
                    project_id:
                      type: string
                      format: uuid
      responses:
        "201":
          description: Erfasste Einzelkosten.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectExpense"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
  /project-expenses/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - costs
      operationId: getProjectExpense
      summary: Einzelkosten lesen
      responses:
        "200":
          description: Einzelkosten.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectExpense"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - costs
      operationId: updateProjectExpense
      summary: Einzelkosten ändern
      description: PATCH-Semantik. Auch nach der Weiterbelastung änderbar — die gestellte Rechnung ist ein Schnappschuss und ändert sich nicht.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectExpenseInput"
      responses:
        "200":
          description: Geänderte Einzelkosten.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectExpense"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - costs
      operationId: deleteProjectExpense
      summary: Einzelkosten löschen
      description: Soft-Delete. Auf einer gestellten Rechnung weiterbelastet → 409.
      responses:
        "200":
          description: Gelöscht.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /orders:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - orders
      operationId: listOrders
      summary: Aufträge auflisten
      description: Neueste zuerst (Keyset-Pagination). Mit `progress=1` trägt jede Zeile zusätzlich `progress` (geleistet, abgerechnet, Reststände, Ampel) und `window` (Zurechnungsfenster).
      parameters:
        - name: status
          in: query
          required: false
          description: "`active` = nicht archiviert (Standard), `all` = auch archivierte, sonst der Auftragsstatus (`open`, `completed`, `cancelled`; jeweils ohne archivierte)."
          schema:
            type: string
            enum:
              - active
              - all
              - open
              - completed
              - cancelled
            default: active
        - name: customer_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: project_id
          in: query
          required: false
          description: Aufträge, die dieses Projekt enthalten (auch zusammen mit weiteren Projekten). Aufträge ohne Projektauswahl (gelten für alle Projekte des Kunden) gehören nicht dazu.
          schema:
            type: string
            format: uuid
        - name: progress
          in: query
          required: false
          description: "`1` = Fortschritt je Auftrag mitliefern."
          schema:
            type: string
            enum:
              - "1"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der Aufträge.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Order"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - orders
      operationId: createOrder
      summary: Auftrag anlegen
      description: "`customer_id`, `title`, `ordered_on` und MINDESTENS eine Obergrenze (`volume_micros` oder `time_budget_seconds`) sind Pflicht; ein Festpreis (`billing_model: fixed_price`) braucht `volume_micros`. `project_ids` grenzt den Auftrag auf Projekte des Kunden ein (fehlt oder leer = alle Projekte des Kunden); ein Projekt eines anderen Kunden → 422. Das Projekt führt die Abrechnungsart: ohne `billing_model` erbt der Auftrag die gemeinsame Art seiner Projekte; ein laufender Auftrag mit anderer Art als seine Projekte oder auf einem Pro-bono-Projekt → 422 (gilt ebenso beim Ändern)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/OrderInput"
                - type: object
                  required:
                    - customer_id
                    - title
                    - ordered_on
      responses:
        "201":
          description: Auftrag angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /orders/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - orders
      operationId: getOrder
      summary: Auftrag inklusive Fortschritt (auch archivierte)
      responses:
        "200":
          description: Auftrag mit `progress` und `window` (fehlen, wenn kein Fenster auswertbar ist) sowie dem Stand der Anzahlung (`advance`).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    patch:
      tags:
        - orders
      operationId: updateOrder
      summary: Auftrag ändern, archivieren oder wiederherstellen
      description: "PATCH-Semantik. `customer_id` darf nicht null werden. `archived: true` archiviert, `archived: false` holt zurück. `project_ids` ersetzt die Projektliste (`[]` = alle Projekte des Kunden); Projekte und Kunde müssen zusammenpassen, sonst 422 — auch, wenn nur `customer_id` wechselt."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/OrderInput"
                - type: object
                  properties:
                    archived:
                      type: boolean
      responses:
        "200":
          description: Geänderter Auftrag (ohne Fortschritt).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - orders
      operationId: archiveOrder
      summary: Auftrag archivieren (Soft-Delete)
      description: Zugeordnete Rechnungen behalten ihren Auftragsbezug. Bereits archiviert → 404.
      responses:
        "200":
          $ref: "#/components/responses/Archived"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /orders/{id}/advance-invoice:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - orders
      operationId: createOrderAdvanceInvoice
      summary: Anzahlungsrechnung zum Auftrag anlegen
      description: "Legt die Anzahlungsrechnung des Auftrags als vorbefüllten Entwurf an: Titel „Anzahlungsrechnung“, `is_advance: true`, Auftragsbezug, Projekt des Auftrags, Leistungszeitraum = Laufzeit des Auftrags und eine Position mit dem vereinbarten Nettobetrag der Anzahlung. Liegt schon ein Entwurf einer Anzahlungsrechnung vor, kommt dieser zurück (200). Ist die Anzahlung bereits gestellt → 409; ohne vereinbarte Anzahlung → 422. Auth: Session mit Schreibrolle."
      responses:
        "200":
          description: Vorhandener Entwurf der Anzahlungsrechnung.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Invoice"
        "201":
          description: Anzahlungsrechnung als Entwurf angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /ai-sessions:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - ai-sessions
      operationId: listAiSessions
      summary: KI-Sessions auflisten
      description: "Nicht gelöschte Sessions, neueste zuerst (nach Anlage). `unassigned` = noch keinem Projekt zugeordnet (Basis der lernenden Zuordnung). Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - assigned
              - unassigned
              - all
            default: all
        - name: project_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der KI-Sessions.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AiSession"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /ai-sessions/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - ai-sessions
      operationId: getAiSession
      summary: KI-Session lesen
      description: "Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert). Gelöschte → 404."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: KI-Session.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/AiSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      tags:
        - ai-sessions
      operationId: updateAiSession
      summary: Billability einer KI-Session überschreiben
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - billability
              properties:
                billability:
                  type:
                    - string
                    - "null"
                  enum:
                    - billable
                    - non_billable
                    - internal
                    - excluded
                    - null
                  description: null bzw. "" = wieder von Projekt/Kunde erben.
      responses:
        "200":
          description: Aktualisierte Session.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/AiSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - ai-sessions
      operationId: deleteAiSession
      summary: KI-Session löschen (Soft-Delete)
      description: Blendet die Session workspace-weit aus (Liste, Dashboard, Reporting). Ein erneuter Ingest belebt sie nicht wieder. Das Free-Kontingent wird dadurch nicht zurückgesetzt.
      responses:
        "200":
          $ref: "#/components/responses/Deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /ai-sessions/{id}/assign:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - ai-sessions
      operationId: assignAiSession
      summary: KI-Session einem Projekt zuweisen (lernend)
      description: "Mit `learn: true` (Standard) entsteht eine Zuordnungsregel — auf den Startordner der Session oder, mit `learn_path`, auf einen geschriebenen Ordner innerhalb des Startordners bzw. das Repository der Session. Künftige Sessions werden danach automatisch zugeordnet; bereits eingegangene unzugeordnete Sessions (nach allen Regeln) und Commits im Teilbaum der Regel werden nachgezogen. Die Session selbst gilt danach als manuell zugeordnet (`project_assignment: manual`): spätere Events mit einem anderen Ordner-Treffer ändern ihr Projekt nicht mehr. Ihre Subagenten (rekursiv, auch später eintreffende) übernehmen das Projekt als `project_assignment: inherited`; Subagenten mit eigener Hand-Zuordnung bleiben, wie sie sind. Nachgezogene Sessions bleiben automatisch."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - project_id
              properties:
                project_id:
                  type: string
                  format: uuid
                  description: Aktives Projekt im Workspace (sonst 422).
                learn:
                  type: boolean
                  default: true
                learn_path:
                  type:
                    - string
                    - "null"
                  maxLength: 1024
                  description: Ziel der gelernten Regel; ohne Angabe der Startordner. Erlaubt sind der Startordner, ein Ordner darin, der einen geschriebenen Ordner (`work_paths`) enthält, und das Repository der Session (`git_remote`) — sonst 422.
      responses:
        "200":
          description: Zugeordnet (inkl. Lern- und Nachzieh-Ergebnis).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - learned
                      - folder_path
                      - reassigned_sessions
                      - reassigned_commits
                    properties:
                      learned:
                        type: boolean
                      folder_path:
                        type:
                          - string
                          - "null"
                        description: Ziel der gelernten Regel (ohne Lernen der Startordner).
                      reassigned_sessions:
                        type: integer
                      reassigned_commits:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /git-commits:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - git-commits
      operationId: listGitCommits
      summary: Git-Commits auflisten
      description: "Neueste zuerst (nach Anlage). Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - assigned
              - unassigned
              - all
            default: all
        - name: project_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite der Commits.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/GitCommit"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /dashboard/summary:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - reporting
      operationId: dashboardSummary
      summary: Aggregierte Auswertung (Zeit, Token, Leistungswert, Kosten)
      description: "Führt Arbeitszeit (Timer), KI-Zeit und Token (KI-Sessions) und die daraus ABGELEITETEN Geldwerte zusammen — gesamt, je Projekt, je Kunde, je Tag, je Monat, je Modell, je Tool und nach Herkunft. Geld in Mikro-Euro (µ€, 1 € = 1.000.000 µ€). Rein lesend. Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - $ref: "#/components/parameters/Range"
        - name: project_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: customer_id
          in: query
          required: false
          description: Nur die Projekte dieses Kunden auswerten.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Auswertung.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/DashboardSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /revenue:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - reporting
      operationId: getRevenue
      summary: Umsatz und Rentabilität im Zeitfenster
      description: "Stellt den kalkulatorischen Leistungswert (Stundensatz × abrechenbare Zeit) dem tatsächlich Fakturierten gegenüber und zieht die Kosten ab (je Projekt nach dessen Kosteneinstellungen: KI-Kosten, KI-Agentenzeit, Arbeitszeit, Einzelkosten) — je Kunde, je Projekt, als Monatsverlauf und gesamt. Vier Begriffe, die nie vermischt werden — `performance_value_micros` (Leistungswert), `invoiced_micros` (Fakturiert, Rechnungen ab `final`), `unbilled_micros` (Leistungswert − Fakturiert, darf negativ sein) und `contribution_micros` (Deckungsbeitrag = Fakturiert − Kosten). Gefiltert wird über das Rechnungsdatum. Entwürfe und Stornos zählen nicht. Auth: Session, Bearer-JWT ODER API-Key."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - $ref: "#/components/parameters/Range"
        - $ref: "#/components/parameters/FromDay"
        - $ref: "#/components/parameters/ToDay"
      responses:
        "200":
          description: Kennzahlen je Kunde, je Projekt, Monatsverlauf und gesamt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/RevenueSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /export:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - reporting
      operationId: exportAuswertung
      summary: Auswertung oder Rohdaten als Datei exportieren (CSV/Excel/PDF)
      description: "Serverseitig erzeugter Download. `kind=summary` (Standard) ist die konfigurierbare Auswertung (`sections`, `include`, `revenue`, `granularity`); die Rohdaten-Arten `timers`, `ai_sessions`, `line_items`, `commits` und `nachweis` liefern Einzelzeilen (`billability`, `include_non_billable`; beim Nachweis wählt `include` die Sektionen). Zeitraum per `range` ODER `from`/`to`, Umfang per `project_id` bzw. `customer_id`. Tarif: frei ist nur `kind=timers&format=csv` (ohne Pro ohne Stundensatz- und Umsatzspalten); alles andere ist Pro — bei eingeschalteten Tarifgrenzen erhalten Nutzer ohne Pro 402 `upgrade_required` (`details.feature = export`). Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: kind
          in: query
          required: false
          schema:
            type: string
            enum:
              - summary
              - timers
              - ai_sessions
              - line_items
              - commits
              - nachweis
            default: summary
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
              - xlsx
              - pdf
            default: csv
        - $ref: "#/components/parameters/Range"
        - $ref: "#/components/parameters/FromDay"
        - $ref: "#/components/parameters/ToDay"
        - name: project_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: customer_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: sections
          in: query
          required: false
          description: Nur `kind=summary`. Kommagetrennt aus summary, projects, models, tools, daily, activities, commits; Standard `summary,projects`. Unbekannte Sektion → 422.
          schema:
            type: string
            example: summary,projects,models,tools,daily,activities
        - name: include
          in: query
          required: false
          description: Bei `kind=summary` kommagetrennt aus cache, subagents, costs (fehlt = nur costs; gesetzt = genau die genannten). Bei `kind=nachweis` aus sessions, timers, commits (Standard alle). Unbekannter Wert → 422.
          schema:
            type: string
        - name: revenue
          in: query
          required: false
          description: Nur `kind=summary` mit Kostenspalten — `0` blendet Umsatz und Deckungsbeitrag aus.
          schema:
            type: string
            enum:
              - "0"
              - "1"
            default: "1"
        - name: granularity
          in: query
          required: false
          description: Nur `kind=summary`, Sektion `daily`. Unbekannter Wert fällt auf `day` zurück.
          schema:
            type: string
            enum:
              - day
              - week
              - month
            default: day
        - name: branding
          in: query
          required: false
          description: Corporate-Design an PDF/Excel (`0` = aus). Bei CSV wirkungslos.
          schema:
            type: string
            enum:
              - "0"
              - "1"
            default: "1"
        - name: billability
          in: query
          required: false
          description: Nur Rohdaten-Arten — auf eine Billability einschränken.
          schema:
            type: string
            enum:
              - billable
              - non_billable
              - internal
              - excluded
        - name: include_non_billable
          in: query
          required: false
          description: Nur `kind=line_items` — auch nicht abrechenbare Positionen.
          schema:
            type: string
            enum:
              - "true"
              - "false"
            default: "false"
      responses:
        "200":
          description: Datei-Download (Content-Disposition attachment).
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            text/csv:
              schema:
                type: string
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /model-prices:
    get:
      tags:
        - models
      operationId: listModelPrices
      summary: Globale Modellpreise (versioniert)
      description: Offizielle Herstellerpreise für alle Workspaces, je Modell mit Gültigkeitsfenster. Rein lesend.
      parameters:
        - $ref: "#/components/parameters/WorkspaceHeader"
      responses:
        "200":
          description: Alle Preisversionen, nach Modell sortiert.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ModelPrice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
  /model-aliases:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - models
      operationId: listModelAliases
      summary: Modell-Aliase auflisten (global und eigene)
      description: Globale Aliase (`workspace_id` null, nur lesbar) und die des Workspaces, sortiert nach Tool und rohem Modellnamen, höchstens 500.
      responses:
        "200":
          description: Aliase.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ModelAlias"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    post:
      tags:
        - models
      operationId: createModelAlias
      summary: Eigenen Modell-Alias anlegen
      description: Ordnet einen rohen Modellnamen eines Tools einem kanonischen Modell der Preistabelle zu, damit dessen Sessions bepreist werden. Doppelte Zuordnung → 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tool
                - raw_model
                - canonical_model
              properties:
                tool:
                  type: string
                  minLength: 1
                  maxLength: 100
                raw_model:
                  type: string
                  minLength: 1
                  maxLength: 200
                canonical_model:
                  type: string
                  minLength: 1
                  maxLength: 200
      responses:
        "201":
          description: Alias angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ModelAlias"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /model-aliases/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    delete:
      tags:
        - models
      operationId: deleteModelAlias
      summary: Eigenen Modell-Alias löschen (endgültig)
      description: Globale Aliase sind nicht löschbar (404).
      responses:
        "200":
          $ref: "#/components/responses/Deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: listInvoices
      summary: Rechnungen auflisten
      description: |
        Rechnungen des Workspaces, absteigend nach Anlagedatum (Keyset-Pagination). Jede Zeile trägt zusätzlich `totals` — die aus den Positionen gerechneten Beträge (Zwischensumme, Nachlass, Netto, Steuer, Gesamtbetrag). Die Summe wird bewusst NICHT gespeichert, damit sie nie von den tatsächlichen Positionen abweichen kann.
      parameters:
        - name: status
          in: query
          required: false
          description: |
            `active` = nicht archiviert (Default), `archived` = nur archivierte, `all` = beides.
          schema:
            type: string
            enum:
              - active
              - archived
              - all
            default: active
        - name: customer_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: project_id
          in: query
          required: false
          description: |
            Nur Rechnungen, deren Umsatzschlüsselung dieses Projekt trägt oder die es in ihrer Projektauswahl (`source_project_ids`) führen — auch Entwürfe.
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite von Rechnungen inkl. gerechneter Beträge.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: "#/components/schemas/Invoice"
                        - type: object
                          required:
                            - totals
                          properties:
                            totals:
                              oneOf:
                                - $ref: "#/components/schemas/InvoiceTotals"
                                - type: "null"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - invoices
      operationId: createInvoice
      summary: Rechnung anlegen
      description: |
        Legt einen Rechnungsentwurf an. `customer_id` ist PFLICHT — ohne Adressat gibt es weder einen Empfänger auf dem Dokument noch eine Adresse, an die der Umsatz in der Rentabilität zurückrollt; fehlt er, antwortet der Server mit 422.
        Außer dem Kunden ist nichts nötig: alles Weitere wird vorbefüllt — Platzhalter-Nummer `ENTWURF-xxxxxxxx` (die echte Nummer vergibt erst das Festschreiben aus dem Nummernkreis der Rechnungsvorlage), heutiges Rechnungsdatum (Europe/Berlin), Empfänger aus dem Kunden (Rechnungsname vor Kundenname), Zahlungsziel des Kunden, sonst das der Vorlage, Steuerbehandlung, Steuersatz und Layout aus der Vorlage und die Standard-Bausteine (`content_config`). Optional `service_from`/`service_to`. Absender, Bank und Steuer-IDs werden NICHT kopiert — ein Entwurf liest sie live aus der Vorlage (`GET /invoice-template`). Mitgesendete Felder gewinnen über die Vorbefüllung. Auth: Session mit Schreibrolle.
        E-RECHNUNG: Eine neue Rechnung übernimmt `einvoice_format` vom Kunden (Standard der Kunden: `zugferd`), dazu Käuferreferenz und Kundennummer (`recipient_customer_number`). Damit wird sie beim Festschreiben als E-Rechnung geprüft und KANN MIT 422 ABGELEHNT WERDEN, wenn Pflichtangaben fehlen (strukturierte Anschriften, IBAN …, siehe `PATCH /invoices/{id}`). Vorab zeigt `GET /invoices/{id}` die Lücken in `einvoice_readiness`. Wer keine E-Rechnung will, sendet `einvoice_format: "none"`. Rechnungen, die vor der E-Rechnung angelegt wurden (auch offene Entwürfe), tragen `none`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/InvoiceInput"
                - type: object
                  required:
                    - customer_id
      responses:
        "201":
          description: Rechnungsentwurf angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: getInvoice
      summary: Rechnung inkl. Positionen und Summen
      description: |
        Rechnung mit Positionen und gerechneten Summen. Bei einem ENTWURF zusätzlich `einvoice_readiness`: was beim Stellen als E-Rechnung fehlen würde (dieselben Meldungen wie die 422 beim Stellen). Standard sind nur die Pflichtangaben; mit `validate=true` wird zusätzlich das XML erzeugt und gegen XSD und Schematron geprüft (dauert länger). Bei gestellten Rechnungen ist das Feld `null` — dort steht der Prüfbericht in `einvoice_validation`.
      parameters:
        - name: validate
          in: query
          required: false
          description: |
            Nur für Entwürfe mit E-Rechnung: `true` prüft zusätzlich das erzeugte XML (XSD + Schematron EN 16931 bzw. XRechnung), statt nur die Pflichtangaben.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Rechnung mit `items`, `totals` und (bei Entwürfen) `einvoice_readiness`.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    patch:
      tags:
        - invoices
      operationId: updateInvoice
      summary: Rechnung ändern
      description: |
        Ändert die übergebenen Felder (PATCH-Semantik). `status` ungleich `draft` schreibt die Rechnung fest und setzt `finalized_at`. `archived: true` archiviert (Soft-Delete). Auth: Session mit Schreibrolle.
        FESTSCHREIBEN (`draft` → `final`/`sent`/`paid`): Zuerst werden die Pflichtangaben geprüft (Absendername und -anschrift, USt-IdNr. oder Steuernummer, Empfänger mit Anschrift, Rechnungsdatum, Leistungszeitraum, mindestens eine Position mit Betrag, Steuersatz > 0 bei Regelbesteuerung). Fehlt etwas, antwortet der Server mit 422 und nennt die Lücken in `details.checks`. Sonst vergibt er bei einer Platzhalter-Nummer die nächste Nummer des Nummernkreises und schreibt Absender, Bank, Steuer-IDs, Einleitung, Schlusstext und Layout der Vorlage als Schnappschuss in die Rechnung — danach ist sie unabhängig von späteren Vorlagenänderungen.
        E-RECHNUNG: Trägt die Rechnung `einvoice_format` `zugferd` oder `xrechnung`, prüft der Server beim Festschreiben zusätzlich die Pflichtangaben der E-Rechnung (strukturierte Anschriften, IBAN, USt-IdNr. bei Reverse Charge, Leitweg-ID bei XRechnung, nur EUR …), erzeugt das XML und prüft es gegen XSD und Schematron (EN 16931, bei XRechnung zusätzlich die KoSIT-Regeln). Mängel → 422 mit `details.field = "einvoice"` und den Befunden in `details.requirements` und `details.findings` (Schema `EInvoiceRejectionDetails`); die Rechnung bleibt Entwurf. Wer bewusst ohne E-Rechnung stellen will, setzt im selben PATCH `einvoice_format: "none"`. Bei Erfolg stehen Zeitpunkt und Bericht in `einvoice_validated_at`/`einvoice_validation`, und das abgelegte PDF ist ein ZUGFeRD-PDF. ACHTUNG: Neue Rechnungen übernehmen das Format des Kunden (Standard `zugferd`) — ihr Festschreiben kann deshalb mit 422 scheitern, wo eine Rechnung ohne E-Rechnung durchginge.
        ANSCHRIFTEN: Text (`recipient_address`/`sender_address`, gedruckt im PDF) und Einzelfelder (im XML der E-Rechnung) laufen nie auseinander. Wird nur ein Baustein geändert, setzt der Server den Text daraus neu zusammen; wird nur der Text geändert und passt er nicht zu den gespeicherten Bausteinen, leert der Server die Bausteine dieser Partei (die E-Rechnung meldet dann „nur als Freitext“).
        ANGEHÄNGTE NACHWEISE (`/invoices/{id}/statements`), die noch Entwurf sind, werden beim Festschreiben im selben Zug mit festgeschrieben (Nummer, Snapshot, PDF wie `POST /statements/{id}/finalize`) — die gestellte Rechnung hängt sie bei jedem Download an und darf sich danach nicht mehr ändern. Hat eine dieser Anlagen keinen Baustein, antwortet der Server VOR jedem Schreiben mit 422 (`details.field = "statements"`, `details.statement_ids`) und nennt die leere Anlage; die Rechnung bleibt Entwurf. Scheitert das Festschreiben eines Nachweises danach doch, wird der gesamte PATCH zurückgenommen.
        Das Festschreiben greift nur an einem Entwurf: Ist die Rechnung inzwischen (parallel) gestellt worden, antwortet der Server mit 409 und schreibt nichts.
        ZURÜCK AUF ENTWURF gibt es nicht: `status: "draft"` an einer gestellten Rechnung endet mit 409. Korrekturen laufen über `POST /invoices/{id}/cancel`.
        Der Statuswechsel trägt zwei Nebenwirkungen: Beim Festschreiben (`final`/`sent`/`paid`) werden die abgerechneten Leistungen GESPERRT, damit sie auf keiner zweiten Rechnung landen; liegt eine davon bereits auf einer fremden Rechnung, endet der Aufruf mit 409 und nennt deren Nummer — es wird dann nichts geschrieben. Zurücksetzen, Stornieren und Archivieren geben die Leistungen wieder frei.
        Ebenfalls beim Festschreiben — sowie bei Änderungen an `discount_bp`, `customer_id`, `service_from`, `service_to` oder `issue_date` — wird die Schlüsselung auf Projekt und Kunde neu gebaut, aus der sich die Rentabilität speist. Scheitert dieser Neubau, wird der gesamte PATCH zurückgenommen: eine festgeschriebene Rechnung ohne Schlüsselung zählte als 0 € Umsatz und ließe sich über die Positions-Routen nicht mehr reparieren.
        NUMMERN BEI EINER RÜCKNAHME: Wird das Festschreiben nach der Nummernvergabe zurückgenommen (Update scheitert, Neubau der Schlüsselung scheitert, ein angehängter Nachweis lässt sich nicht festschreiben, oder ein paralleler Aufruf war schneller), gibt der Server die in diesem Aufruf vergebenen Rechnungs- und Nachweisnummern an ihren Nummernkreis zurück; der nächste Beleg bekommt sie wieder. Die Nummernkreise bleiben so lückenlos.
        An einer FESTGESCHRIEBENEN Rechnung sind nur noch `status`, `finalized_at` und `archived` änderbar; jedes andere Feld endet mit 409. Sonst ließe sich der Kunde einer versendeten Rechnung umhängen — die Schlüsselung folgte dem neuen Kunden, der gedruckte Empfänger bliebe der alte.
        `status: "cancelled"` ist hierüber NICHT mehr der gedachte Weg: ein Storno erzeugt seit `POST /invoices/{id}/cancel` einen eigenen Beleg. Zwei Übergänge sind zusätzlich gesperrt (409): jede gestellte Rechnung zurück auf `draft` (Stornobelege eingeschlossen) und eine stornierte Rechnung heraus aus `cancelled`, solange ihr Stornobeleg aktiv ist (sonst stünden beide Belege gleichzeitig gültig da).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/InvoiceInput"
                - type: object
                  properties:
                    archived:
                      type: boolean
                      description: true = archivieren (gibt Sperren frei), false = wiederherstellen.
      responses:
        "200":
          description: Geänderte Rechnung (ohne Positionen).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Entweder liegt beim Festschreiben mindestens eine Leistung dieser Rechnung bereits auf einer anderen (`details.invoice_numbers` nennt die kollidierenden Rechnungsnummern), es wurde ein Inhaltsfeld an einer bereits festgeschriebenen Rechnung geändert, eine gestellte Rechnung sollte zurück auf Entwurf, oder die Rechnung wurde während des Festschreibens bereits anderweitig gestellt (`details.field = "status"`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: |
            Validierungsfehler (`validation_error`). Beim Festschreiben: fehlende Pflichtangaben (`details.field = "status"`, Liste in `details.checks`), ein eigenes Layout ohne alle Pflichtangaben (`details.field = "layout"`) oder eine E-Rechnung mit Mängeln (`details.field = "einvoice"`, siehe `EInvoiceRejectionDetails`). Sonst nennt `details.field` das ungültige Feld.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error:
                        type: object
                        properties:
                          details:
                            oneOf:
                              - $ref: "#/components/schemas/EInvoiceRejectionDetails"
                              - type: object
                                additionalProperties: true
    delete:
      tags:
        - invoices
      operationId: archiveInvoice
      summary: Rechnung archivieren (Soft-Delete) oder endgültig löschen
      description: |
        Standard ist die Archivierung: die Nummer bleibt vergeben, das Dokument nachvollziehbar, die Sperrvermerke werden gelöst.
        Mit `hard=true` wird die Rechnung wirklich entfernt — samt Positionen, Umsatzschlüsselung, Sperrvermerken, Nachweis-Verknüpfungen und einem etwaigen Stornobeleg; die Rechnungsnummer wird wieder frei. Das ist nur erlaubt, wenn der Account den Aufbewahrungsmodus `delete` gesetzt hat (`GET/PUT /workspace/settings`). Entwürfe sind davon ausgenommen und in beiden Modi löschbar — außer ein Entwurf trägt schon eine Nummer aus dem Nummernkreis (409, sonst entstünde eine Lücke; archivieren geht). Ein Stornobeleg lässt sich nie einzeln löschen — er verschwindet nur zusammen mit der Rechnung, die er storniert.
      parameters:
        - name: hard
          in: query
          required: false
          description: |
            `true` löscht endgültig statt zu archivieren — samt der Nachweis-Entwürfe, die aus dieser Rechnung entstanden sind und an keiner anderen hängen. Alles andere (auch das Weglassen) archiviert.
          schema:
            type: string
            enum:
              - "true"
      responses:
        "200":
          description: Rechnung archiviert (`archived`) bzw. endgültig gelöscht (`deleted`).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - id
                    properties:
                      id:
                        type: string
                        format: uuid
                      archived:
                        type: boolean
                        const: true
                      deleted:
                        type: boolean
                        const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Endgültiges Löschen nicht erlaubt — der Account arbeitet auditfähig, oder es handelt sich um einen Stornobeleg.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /invoices/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - invoices
      operationId: cancelInvoice
      summary: Rechnung stornieren (erzeugt einen Stornobeleg)
      description: |
        Erzeugt eine STORNORECHNUNG: eine eigene Rechnung mit eigener Nummer aus dem Nummernkreis, denselben Positionen in negativer Höhe (Menge und Betrag negiert, der Einzelpreis bleibt) und einem Verweis auf das Original (`cancels_invoice_id`). Empfänger, Absender, Steuermodus, Nachlass und Layout werden übernommen; Fälligkeit, Zahlungsziel, Zahlungshinweis und Schlusstext entfallen. Der Beleg entsteht bereits festgeschrieben (`status: "final"`) und lässt sich nicht mehr in einen Entwurf zurückverwandeln. Scheitert der Storno, nachdem die Nummer vergeben wurde, wird der halbfertige Beleg entfernt und die Nummer an den Nummernkreis zurückgegeben.
        Das Original wechselt auf `status: "cancelled"`, bekommt `cancelled_at` und `cancel_reason` und gibt seine Sperrvermerke frei — die abgerechneten Zeiten und KI-Sessions sind danach wieder verfügbar. Seine Umsatzschlüsselung bleibt bewusst stehen (nachvollziehbar), zählt über den Status aber nicht mehr mit. Auch der Stornobeleg zählt nicht mit: Original und Storno heben sich zu 0 € auf, ohne den Umsatz doppelt zu korrigieren.
        Möglich nur für festgeschriebene, versendete oder bezahlte Rechnungen, die weder archiviert noch bereits storniert sind und selbst kein Stornobeleg sind. Unabhängig vom Aufbewahrungsmodus des Accounts.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 1000
                  description: |
                    Grund der Stornierung. Wird auf dem Stornobeleg unter dem Bezugstext genannt und am Original als `cancel_reason` vermerkt.
      responses:
        "201":
          description: "`data.invoice` ist die stornierte Rechnung, `data.cancellation` der neue Stornobeleg."
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - invoice
                      - cancellation
                    properties:
                      invoice:
                        $ref: "#/components/schemas/Invoice"
                      cancellation:
                        $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Storno nicht möglich (Entwurf, bereits storniert, archiviert, selbst ein Stornobeleg) oder es existiert bereits ein Stornobeleg — dessen Nummer steht dann in der Meldung.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/candidates:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: getInvoiceCandidates
      summary: Auswahlgrundlage des Rechnungs-Baukastens
      description: |
        Kunde mit Stundensatz und KI-Laufzeitsatz, alle nicht archivierten Projekte des Kunden (mit `selected` gemäß `source_project_ids`), die Zeiteinträge und KI-Sessions der gewählten Projekte im Leistungszeitraum samt Sperrvermerk (`billedOn` = Nummer der Rechnung, auf der die Leistung schon liegt) und `lastBilledTo` (Leistungsende der letzten gestellten Rechnung des Kunden). Ohne Leistungszeitraum kommen leere Listen statt 422.
      responses:
        "200":
          description: Auswahlgrundlage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceCandidates"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
  /invoices/{id}/content:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    put:
      tags:
        - invoices
      operationId: updateInvoiceContent
      summary: Bausteine eines Entwurfs speichern
      description: |
        Speichert `config` (unbekannte Schlüssel fallen weg, ungültige Werte auf den Standard), bildet die maschinellen Positionen (`time`, `ai_tokens`, `ai_cost`, `ai_runtime`, `expense`) mit derselben Funktion neu, mit der die Oberfläche die Live-Summe rechnet, setzt sie vor die eigenen Positionen und Hinweistexte, bildet die Rabatte (`discount`, hinter allen anderen Positionen) neu, setzt Anzahlungsabzüge (`advance`) ans Ende und baut die Umsatzschlüsselung neu. Leistungen auf anderen Rechnungen werden nie übernommen. Nur bei Entwürfen (sonst 409). Auth: Session mit Schreibrolle.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - config
              properties:
                config:
                  $ref: "#/components/schemas/InvoiceContentConfig"
      responses:
        "200":
          description: Gespeicherte Konfiguration, Positionen danach und Hinweise zur Auswahl.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - config
                      - items
                      - warnings
                    properties:
                      config:
                        $ref: "#/components/schemas/InvoiceContentConfig"
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/InvoiceItem"
                      warnings:
                        type: array
                        items:
                          $ref: "#/components/schemas/ContentWarning"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Rechnung ist gestellt und inhaltlich gesperrt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/generate:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - invoices
      operationId: generateInvoiceItems
      summary: Positionen aus erfassten Leistungen erzeugen
      description: |
        Wertet den Leistungszeitraum der Rechnung aus (`service_from`/`service_to`, Pflicht — sonst 422) und bildet daraus Positionen. Die Bewertung nutzt exakt dieselben Funktionen wie jede andere Auswertung: Billability geerbt, Zeiten nach Projekt-/Kundenregel gerundet, KI-Kosten über die versionierten Modellpreise abgeleitet. Der Umfang folgt `source_project_ids` der Rechnung, sonst den Projekten ihres Kunden, sonst dem ganzen Workspace. `replace` (Default true) ersetzt nur die maschinell erzeugten Positionen (`time`, `ai_tokens`, `ai_cost`, `ai_runtime`, `expense`) — von Hand angelegte Positionen und Textblöcke bleiben stehen. Rabatte (`discount`) werden danach aus `content_config.discounts` neu gebildet.
        Die Oberfläche nutzt diese Route nicht mehr (sie speichert Bausteine über `PUT /invoices/{id}/content`); sie bleibt für API-Nutzer bestehen.
        Leistungen, die bereits auf einer anderen Rechnung abgerechnet sind, werden übersprungen und in `evaluated.skipped_billed` gemeldet. Mit `replace: false` gilt das auch für die Leistungen, die schon in den Positionen DIESER Rechnung stecken — sonst entstünden sie ein zweites Mal, und die Rechnung stellte dieselben Stunden doppelt in Rechnung. Wer vorher sehen will, was entsteht, nutzt `GET /invoices/{id}/preview` — dieselben Optionen, ohne zu schreiben.
        Nur bei Entwürfen erlaubt.
      requestBody:
        required: true
        description: JSON-Objekt; `{}` erzeugt mit den Standardoptionen.
        content:
          application/json:
            schema:
              type: object
              properties:
                include_time:
                  type: boolean
                  default: true
                time_grouping:
                  type: string
                  enum:
                    - project
                    - day
                    - entry
                    - rate
                  default: project
                  description: |
                    `project` = eine Position je Projekt, `day` = je Tag × Projekt, `entry` = je Zeiteintrag (Titel = Beschreibung), `rate` = eine Position je Stundensatz. Bei gemischten Sätzen ließen sich eine Menge und ein Einzelpreis für alles zusammen nicht darstellen — deshalb keine Gruppierung „alles in einer Zeile".
                only_billable_time:
                  type: boolean
                  default: true
                  description: |
                    false nimmt auch nicht abrechenbare Zeit auf — dann mit der erfassten (ungerundeten) Dauer.
                ai_mode:
                  type: string
                  enum:
                    - none
                    - cost
                    - tokens
                    - runtime
                  default: none
                  description: |
                    `cost` = entstandene KI-Kosten als Pauschalposition weiterberechnen, `tokens` = nach Token-Menge zu einem eigenen Preis abrechnen, `runtime` = aktive Agentenlaufzeit × Stundensatz (Positionsart `ai_runtime`).
                ai_grouping:
                  type: string
                  enum:
                    - project
                    - model
                    - single
                  default: project
                ai_markup_bp:
                  type: integer
                  minimum: 0
                  maximum: 100000
                  default: 0
                  description: Aufschlag auf durchgereichte KI-Kosten in Basispunkten (2000 = 20 %).
                ai_token_price_micros:
                  type: integer
                  minimum: 0
                  description: |
                    Preis je 1.000.000 Netto-Token in µ€. Pflicht (> 0) bei `ai_mode: tokens`.
                ai_runtime_rate_cents:
                  type: integer
                  minimum: 0
                  maximum: 1000000
                  description: |
                    Stundensatz der aktiven KI-Laufzeit in Cent. Pflicht (> 0) bei `ai_mode: runtime`.
                replace:
                  type: boolean
                  default: true
      responses:
        "200":
          description: |
            Anzahl erzeugter Positionen, die vollständige Positionsliste und `evaluated` (ausgewertete Timer/Sessions sowie `skipped_billed`) — damit ein leeres oder unerwartet mageres Ergebnis erklärbar bleibt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - generated
                      - items
                      - evaluated
                    properties:
                      generated:
                        type: integer
                        description: Anzahl neu erzeugter Positionen.
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/InvoiceItem"
                      evaluated:
                        type: object
                        properties:
                          timers:
                            type: integer
                          sessions:
                            type: integer
                          project_ids:
                            type: array
                            items:
                              type: string
                              format: uuid
                          dropped_project_ids:
                            type: array
                            description: Gewählte Projekte, die nicht zum Rechnungskunden gehören (oder nicht mehr existieren) und deshalb nicht ausgewertet wurden.
                            items:
                              type: string
                              format: uuid
                          entity_ids_truncated:
                            type: boolean
                            description: |
                              true = mindestens eine Position führt mehr Leistungen, als ihre Herkunftsliste fasst (2000 je Position). Für sie wird KEIN Sperrvermerk gegen Doppelfakturierung geschrieben — ihre Leistungen lassen sich danach ein zweites Mal abrechnen, ohne dass ein 409 dazwischengeht. Abhilfe: engerer Zeitraum oder Gruppierung nach Projekt.
                          skipped_billed:
                            $ref: "#/components/schemas/SkippedBilled"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Rechnung ist festgeschrieben und inhaltlich gesperrt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/preview:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: previewInvoiceItems
      summary: Positionsvorschau — zeigt, was `/generate` erzeugen würde
      description: |
        Wertet denselben Leistungszeitraum mit denselben Optionen aus wie `POST /invoices/{id}/generate` (geprüft von derselben Funktion, sonst zeigte die Vorschau etwas anderes als das Ergebnis) und SCHREIBT NICHTS. Damit ist vorab sichtbar, welche Positionen entstehen, wie sich der Betrag über `by_project` auf die Projekte verteilt und wie viel übersprungen wird, weil es bereits auf einer anderen Rechnung liegt (`evaluated.skipped_billed`).
        `by_project` und `net_micros` beziehen sich auf die Rechnung, WIE SIE DANACH AUSSÄHE — erzeugte Positionen plus die, die stehen bleiben. Nur so stimmt der anteilig verteilte Nachlass, denn er hängt an der ganzen Rechnung und nicht an den neuen Zeilen.
        Die Optionen sind dieselben wie im Body von `/generate`, hier als Query-Parameter. Lesend, deshalb ohne Schreibrolle.
      parameters:
        - name: include_time
          in: query
          required: false
          schema:
            type: boolean
            default: true
        - name: time_grouping
          in: query
          required: false
          schema:
            type: string
            enum:
              - project
              - day
              - entry
              - rate
            default: project
        - name: only_billable_time
          in: query
          required: false
          schema:
            type: boolean
            default: true
        - name: ai_mode
          in: query
          required: false
          schema:
            type: string
            enum:
              - none
              - cost
              - tokens
              - runtime
            default: none
        - name: ai_grouping
          in: query
          required: false
          schema:
            type: string
            enum:
              - project
              - model
              - single
            default: project
        - name: ai_markup_bp
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 100000
        - name: ai_token_price_micros
          in: query
          required: false
          description: Preis je 1.000.000 Netto-Token in µ€. Pflicht (> 0) bei `ai_mode=tokens`.
          schema:
            type: integer
            minimum: 0
        - name: ai_runtime_rate_cents
          in: query
          required: false
          description: Stundensatz der KI-Laufzeit in Cent. Pflicht (> 0) bei `ai_mode=runtime`.
          schema:
            type: integer
            minimum: 0
        - name: replace
          in: query
          required: false
          description: |
            Bestimmt, welche bestehenden Positionen in `by_project`/`net_micros` mitgezählt werden — true (Default) lässt nur `manual` und `text` stehen.
          schema:
            type: boolean
            default: true
      responses:
        "200":
          description: Vorschau der Positionen inkl. Aufteilung je Projekt.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/InvoicePreview"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/items:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: listInvoiceItems
      summary: Positionen einer Rechnung
      responses:
        "200":
          description: Positionen in Anzeigereihenfolge.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/InvoiceItem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - invoices
      operationId: addInvoiceItem
      summary: Position anhängen
      description: |
        Hängt eine Position ans Ende. Ohne `amount_micros` rechnet der Server `quantity_milli × unit_price_micros ÷ 1000` — die gedruckte Zeile muss aufgehen. Nur bei Entwürfen erlaubt (festgeschriebene Rechnung → 409).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/InvoiceItemInput"
                - type: object
                  required:
                    - title
      responses:
        "201":
          description: Position angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceItem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Rechnung ist festgeschrieben und inhaltlich gesperrt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/items/{itemId}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - name: itemId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - $ref: "#/components/parameters/WorkspaceHeader"
    patch:
      tags:
        - invoices
      operationId: updateInvoiceItem
      summary: Position ändern
      description: |
        Ändern sich `quantity_milli` oder `unit_price_micros`, ohne dass ein `amount_micros` mitgesendet wird, rechnet der Server den Betrag neu — sonst stünde eine Zeile auf der Rechnung, die nicht mehr aufgeht.
        `project_id`/`project_name` ändern bei einer ERZEUGTEN Position, die bereits je Projekt aufgeteilt ist (`source.allocations`, etwa „Arbeitszeit (120,00 €/h)" über mehrere Projekte), nur die Beschriftung auf der Rechnung: Der Betrag folgt weiterhin der gespeicherten Aufteilung, denn sie hält fest, an welchen Projekten die Leistung tatsächlich erbracht wurde. Wer den Umsatz auf ein anderes Projekt buchen will, ersetzt die Position durch eine manuelle.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceItemInput"
      responses:
        "200":
          description: Position geändert.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceItem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Rechnung ist festgeschrieben und inhaltlich gesperrt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - invoices
      operationId: deleteInvoiceItem
      summary: Position entfernen
      description: |
        Positionen eines Entwurfs werden hart gelöscht — sie sind ein Vorschlag, kein Beleg.
      responses:
        "200":
          description: Position entfernt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - id
                      - deleted
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
                        const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Rechnung ist festgeschrieben und inhaltlich gesperrt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /invoices/{id}/export:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: exportInvoice
      summary: Rechnung als Datei (PDF/Excel/CSV/E-Rechnung)
      description: |
        Serialisiert dasselbe Dokument, das die Web-Ansicht zeigt — Bildschirm und Datei können nicht auseinanderlaufen. Angehängte Leistungsnachweise beginnen im PDF auf eigenen Seiten, in Excel als eigene Blätter; mit `parts` liefert das PDF nur die Rechnung oder nur die Leistungsnachweise. Tarif: gestellte Rechnungen bleiben auch ohne Pro abrufbar (Archiv); der Export eines Entwurfs ist Pro (402 `upgrade_required`, Feature `invoices`).
        E-RECHNUNG: Das PDF einer gestellten Rechnung mit `einvoice_format` ≠ `none` ist ein ZUGFeRD-PDF (PDF/A-3, eingebettetes CII-XML, Profil EN 16931). `format=xrechnung` liefert die XRechnung als XML (CII, mit `syntax=ubl` in UBL), `format=zugferd-xml` das in das PDF eingebettete CII-XML. Beide nur für gestellte Rechnungen mit E-Rechnung (sonst 409).
        Die XRechnung wird vor der Auslieferung gegen das Profil XRechnung (KoSIT-Regeln) geprüft — auch bei Rechnungen, die als ZUGFeRD gestellt und dabei nur gegen EN 16931 geprüft wurden. Fehlt etwas, was nur XRechnung verlangt (Leitweg-ID, E-Mail des Empfängers, Telefon des Absenders …), antwortet der Server mit 422 und `error.details` wie beim Stellen (`EInvoiceRejectionDetails`). An einer gestellten Rechnung lassen sich diese Angaben nicht mehr ergänzen; die gültige E-Rechnung bleibt dann das ZUGFeRD-PDF.
      parameters:
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - pdf
              - xlsx
              - csv
              - xrechnung
              - zugferd-xml
            default: pdf
        - name: syntax
          in: query
          required: false
          description: Nur bei `format=xrechnung` — Syntax des XML.
          schema:
            type: string
            enum:
              - cii
              - ubl
            default: cii
        - name: parts
          in: query
          required: false
          description: |
            Nur bei `format=pdf` — welche Teile das PDF enthält. `all` = Rechnung mit den verknüpften Leistungsnachweisen als Anlage (eigene Seiten im Layout des Nachweises), `invoice` = nur die Rechnung (bei E-Rechnung weiterhin das ZUGFeRD-PDF), `statements` = nur die verknüpften Leistungsnachweise in einer Datei (ohne E-Rechnung, kein Header `X-EInvoice`; 409, wenn keiner verknüpft ist). Bei anderen Formaten nur `all` (sonst 422).
          schema:
            type: string
            enum:
              - all
              - invoice
              - statements
            default: all
      responses:
        "200":
          description: Datei-Download (Content-Disposition attachment).
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-EInvoice:
              description: |
                Nur bei `format=pdf` einer gestellten Rechnung mit E-Rechnung. `embedded` = das PDF ist ein ZUGFeRD-PDF mit eingebettetem XML. `missing` = technischer Ausfall (Ersatz-PDF ohne Chromium oder Einbetten gescheitert): das PDF enthält KEINE E-Rechnung und wird nicht abgelegt — später erneut abrufen, bevor es versendet wird.
              schema:
                type: string
                enum:
                  - embedded
                  - missing
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            text/csv:
              schema:
                type: string
            application/xml:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            E-Rechnungs-XML angefordert, aber die Rechnung ist noch ein Entwurf oder wurde ohne E-Rechnung gestellt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: |
            Ungültiger Parameter, oder die XRechnung besteht die Prüfung nicht — dann mit `error.details` nach `EInvoiceRejectionDetails` (`field: einvoice`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /invoices/{id}/statements:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoices
      operationId: listInvoiceStatements
      summary: Nachweise, die dieser Rechnung als Anlage beiliegen
      description: |
        Die Verknüpfungen in Anzeigereihenfolge, jeweils mit dem Nachweis selbst (ohne Snapshot). `display_label` ist die gespeicherte Beschriftung, sonst die laufende Nummerierung („Anlage 1“) — sie folgt der POSITION in der Liste, damit nach dem Lösen der ersten von drei Anlagen wieder bei 1 gezählt wird.
      responses:
        "200":
          description: Verknüpfte Nachweise in Anzeigereihenfolge.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/InvoiceStatementLink"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - invoices
      operationId: attachInvoiceStatement
      summary: Nachweis verknüpfen oder aus der Rechnung erzeugen
      description: |
        Entweder `statement_id` (bestehenden Nachweis verknüpfen) ODER `create: true` (einen neuen aus der Rechnung erzeugen) — beides zusammen ist ein 422, sonst gewänne stillschweigend eines von beidem.
        Beim Verknüpfen muss der Nachweis demselben Kunden gehören wie die Rechnung: die Anlage geht mit an genau diesen Empfänger, fremde Kundenarbeit darin wäre eine Offenlegung gegenüber einem Dritten (422). Ein archivierter Nachweis wird ebenfalls abgewiesen.
        Beim Erzeugen (`create: true`) entsteht ein Leistungsnachweis wie bei `POST /statements { from_invoice_id }`: Kunde, Leistungszeitraum (ohne vollständigen Zeitraum: Vormonat), Projektauswahl, Auftrag und Empfänger der Rechnung, die Auswahl folgt der Rechnung (`selection.source = invoice`, live solange beide Entwürfe sind), die Darstellung ist die eines Einzelnachweises. Mit `POST /statements/{id}/detach` lässt sich die Auswahl später lösen. Die Rechnung braucht einen Kunden (sonst 422).
        Nur an einem ENTWURF erlaubt (409 sonst): die Anlagen sind Teil des Dokuments — eine festgeschriebene Rechnung sähe beim nächsten Export sonst anders aus als die, die beim Kunden liegt.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceStatementInput"
      responses:
        "201":
          description: Nachweis verknüpft (bzw. erzeugt und verknüpft).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceStatementLink"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Rechnung ist festgeschrieben und inhaltlich gesperrt, oder dieser Nachweis ist bereits mit ihr verknüpft.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/statements/{statementId}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    delete:
      tags:
        - invoices
      operationId: detachInvoiceStatement
      summary: Verknüpfung zwischen Rechnung und Nachweis lösen
      description: |
        Löst nur die VERKNÜPFUNG. Der Nachweis selbst bleibt bestehen: er ist ein eigenständiges Dokument mit eigener Nummer, hängt womöglich noch an einer weiteren Rechnung und wurde vielleicht längst an den Kunden gegeben. Archiviert wird er über `DELETE /statements/{id}`. Nur an einem Entwurf erlaubt.
      parameters:
        - name: statementId
          in: path
          required: true
          description: ID des NACHWEISES (nicht der Verknüpfung).
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Verknüpfung gelöst.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - id
                      - statement_id
                      - detached
                    properties:
                      id:
                        type: string
                        format: uuid
                        description: ID der Verknüpfung.
                      statement_id:
                        type: string
                        format: uuid
                      detached:
                        type: boolean
                        const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Rechnung unbekannt oder dieser Nachweis ist mit ihr nicht verknüpft.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Rechnung ist festgeschrieben und inhaltlich gesperrt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /invoices/{id}/payments:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - payments
      operationId: listInvoicePayments
      summary: Zahlungseingänge einer Rechnung samt Zahlungsstand
      description: |
        Alle erfassten Zahlungen der Rechnung (ältester Eingang zuerst) und der daraus abgeleitete Zahlungsstand. `summary.display_status` ergänzt den gespeicherten Status um die zwei abgeleiteten Zustände `partial` (teilweise bezahlt) und `overdue` (Fälligkeit überschritten, noch Betrag offen).
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - summary
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/InvoicePayment"
                  summary:
                    $ref: "#/components/schemas/InvoicePaymentSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - payments
      operationId: createInvoicePayment
      summary: Zahlungseingang erfassen
      description: |
        Erfasst eine Zahlung (Eingangstag, Bruttobetrag, optionale Notiz). Das Erfassen ist optional — eine Rechnung lässt sich weiterhin per `PATCH /invoices/{id}` mit `status: "paid"` direkt als bezahlt markieren. Deckt die Summe aller Zahlungen danach den Bruttobetrag, wechselt eine gestellte oder versendete Rechnung auf `paid` (`invoice_status` in der Antwort). Möglich an gestellten, versendeten und bezahlten Rechnungen, die weder archiviert noch storniert und selbst kein Stornobeleg sind.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoicePaymentCreate"
      responses:
        "201":
          description: Angelegt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoicePaymentResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: An diese Rechnung gehört keine Zahlung (Entwurf, storniert, archiviert, Stornobeleg).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoices/{id}/payments/{paymentId}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - name: paymentId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - $ref: "#/components/parameters/WorkspaceHeader"
    patch:
      tags:
        - payments
      operationId: updateInvoicePayment
      summary: Zahlungseingang korrigieren
      description: |
        Ändert Eingangstag, Betrag oder Notiz. Der Status wird danach wie beim Erfassen nachgezogen.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoicePaymentInput"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoicePaymentResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: An diese Rechnung gehört keine Zahlung (mehr).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - payments
      operationId: deleteInvoicePayment
      summary: Zahlungseingang entfernen
      description: |
        Entfernt die Zahlung — auch an einer inzwischen stornierten oder archivierten Rechnung. Deckt die Summe danach den Bruttobetrag nicht mehr, wechselt eine durch Zahlungen bezahlte Rechnung zurück auf `sent`. Eine von Hand bezahlte Rechnung ohne jede Zahlung bleibt bezahlt.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - invoice_status
                  - summary
                properties:
                  data:
                    type: object
                    required:
                      - id
                      - deleted
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
                        const: true
                  invoice_status:
                    type: string
                    enum:
                      - draft
                      - final
                      - sent
                      - paid
                      - cancelled
                  summary:
                    $ref: "#/components/schemas/InvoicePaymentSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /payments:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - payments
      operationId: listPayments
      summary: Zahlungseingänge des Workspaces
      description: |
        Alle erfassten Zahlungen des Workspaces, neueste zuerst (höchstens 200), je mit dem Kopf ihrer Rechnung. Rechnungen, die nur von Hand auf `paid` gesetzt wurden, tragen keine Zahlungen und erscheinen hier nicht.
      parameters:
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Frühester Eingangstag (inklusive, YYYY-MM-DD).
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Spätester Eingangstag (inklusive).
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: "#/components/schemas/InvoicePayment"
                        - type: object
                          properties:
                            invoice:
                              type:
                                - object
                                - "null"
                              properties:
                                id:
                                  type: string
                                  format: uuid
                                invoice_number:
                                  type: string
                                recipient_name:
                                  type:
                                    - string
                                    - "null"
                                customer_id:
                                  type:
                                    - string
                                    - "null"
                                  format: uuid
                                currency:
                                  type: string
                                  enum:
                                    - EUR
                                    - USD
                                    - CHF
                                issue_date:
                                  type: string
                                  format: date
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - payments
      operationId: createPaymentTransfer
      summary: Zahlung auf mehrere Rechnungen verteilen
      description: |
        Erfasst EINE Zahlung — etwa eine Überweisung, mit der ein Kunde mehrere Rechnungen auf einmal begleicht — und verteilt sie auf die genannten Rechnungen. Je Rechnung entsteht eine Zahlung mit ihrem Anteil; bei mehr als einer Rechnung tragen alle dieselbe `transfer_id`. Alle Rechnungen werden vorab geprüft (wie bei `POST /invoices/{id}/payments`) und müssen dieselbe Währung haben; geschrieben wird alles oder nichts. Deckt eine Rechnung danach ihren Bruttobetrag, wechselt sie auf `paid`. Mit nur einer Rechnung entspricht der Aufruf einer Einzelzahlung.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentTransferCreate"
      responses:
        "201":
          description: Angelegt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentTransferResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: An eine der Rechnungen gehört keine Zahlung (Entwurf, storniert, archiviert, Stornobeleg).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /payments/transfers/{transferId}:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
      - name: transferId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Gemeinsame `transfer_id` der Anteile einer Sammelzahlung.
    delete:
      tags:
        - payments
      operationId: deletePaymentTransfer
      summary: Sammelzahlung mit allen Anteilen entfernen
      description: |
        Entfernt alle Zahlungen mit dieser `transfer_id` — also eine auf mehrere Rechnungen verteilte Zahlung als Ganzes (auch Anteile an inzwischen stornierten Rechnungen). Jede beteiligte Rechnung zieht danach ihren Status nach: fehlt Geld, geht eine durch Zahlungen bezahlte Rechnung auf `sent` zurück.
      responses:
        "200":
          description: Entfernt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - transfer_id
                      - deleted
                      - total_micros
                      - invoices
                    properties:
                      transfer_id:
                        type: string
                        format: uuid
                      deleted:
                        type: integer
                        description: Anzahl entfernter Anteile.
                      total_micros:
                        type: integer
                        description: Summe der entfernten Anteile.
                      invoices:
                        type: array
                        items:
                          type: object
                          required:
                            - invoice_id
                            - invoice_status
                            - summary
                          properties:
                            invoice_id:
                              type: string
                              format: uuid
                            invoice_status:
                              type: string
                              enum:
                                - draft
                                - final
                                - sent
                                - paid
                                - cancelled
                            summary:
                              $ref: "#/components/schemas/InvoicePaymentSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /payments/outstanding:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - payments
      operationId: listOutstandingInvoices
      summary: Außenstände — offene Rechnungen samt Zahlungsstand
      description: Aktive, gestellte oder versendete Rechnungen (keine Stornobelege), bei denen nach Abzug der erfassten Zahlungen noch etwas offen ist — älteste Fälligkeit zuerst, höchstens 200. Grundlage für „Zahlung eintragen“.
      responses:
        "200":
          description: Offene Rechnungen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required:
                        - invoice
                        - summary
                      properties:
                        invoice:
                          $ref: "#/components/schemas/Invoice"
                        summary:
                          $ref: "#/components/schemas/InvoicePaymentSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
  /statements:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - statements
      operationId: listStatements
      summary: Nachweise auflisten
      description: |
        Nachweise des Workspaces, absteigend nach Anlagedatum (Keyset-Pagination). Bewusst OHNE `snapshot` — er ist ein vollständiges Dokument und würde die Liste um ein Vielfaches aufblähen, ohne dass ein Zeichen davon angezeigt wird.
      parameters:
        - name: customer_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - draft
              - final
        - name: kind
          in: query
          required: false
          schema:
            type: string
            enum:
              - proof
              - progress
        - name: archived
          in: query
          required: false
          description: |
            `false` = nicht archiviert (Default), `true` = nur archivierte, `all` = beides. Eigener Parameter, weil `status` beim Nachweis den Lebenszyklus trägt und nicht die Sichtbarkeit.
          schema:
            type: string
            enum:
              - "false"
              - "true"
              - all
            default: "false"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Seite von Nachweisen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Statement"
                  next_cursor:
                    type:
                      - string
                      - "null"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - statements
      operationId: createStatement
      summary: Nachweis-Entwurf anlegen (Schritt 1 des Assistenten)
      description: |
        Zwei Formen:
        **Neuer Nachweis** `{ customer_id, kind?, period_from?, period_to?, … }` — `customer_id` ist PFLICHT (sonst 422): ein Nachweis ohne Adressaten wäre keine Urkunde, sondern eine Auswertung. Der Zeitraum darf fehlen (dann gilt der Vormonat nach Europe/Berlin); wer einen angibt, gibt beide Grenzen an (422 sonst). Vorbefüllt werden: Platzhalternummer `ENTWURF-…` (die echte vergibt erst das Festschreiben aus dem Nachweis-Nummernkreis der Vorlage), Titel aus der Art, Empfänger aus dem Kunden und die Startkonfiguration der Bausteine (`content_config`: Kennzahlen + Arbeitszeit, KI-Nutzung, wenn im Rahmen KI-Sessions liegen). `wizard_step` startet auf `rahmen`. Mitgesendete Felder gewinnen über die Vorbefüllung; eine eigene `statement_number` muss im Workspace frei sein (409).
        **Aus einer Rechnung** `{ from_invoice_id, kind?, title? }` — Kunde, Leistungszeitraum (ohne vollständigen Zeitraum: Vormonat), Projektauswahl und Auftrag der Rechnung werden übernommen, `source_invoice_id` gesetzt. Die AUSWAHL folgt der Rechnung (`content_config.selection.source = invoice`, live solange beide Entwürfe sind), die Darstellung ist die eines Einzelnachweises; eigene Positionen der Rechnung werden eigene Leistungen ohne Betrag. `wizard_step` startet auf `inhalte`. Ist die Rechnung ein Entwurf, wird der Nachweis zugleich als Anlage verknüpft (`invoice_link`), an einer gestellten Rechnung bleibt es beim Bezug (`invoice_link: null`). Ein mitgesendetes `customer_id`, das nicht zum Kunden der Rechnung passt, ist ein 422.
        Eine Projektauswahl (`project_ids`) darf ausschließlich Projekte dieses Kunden enthalten (422) — sonst stünde fremde Arbeit in einem Dokument, das an einen Dritten geht.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - allOf:
                    - $ref: "#/components/schemas/StatementInput"
                    - type: object
                      required:
                        - customer_id
                - type: object
                  required:
                    - from_invoice_id
                  properties:
                    from_invoice_id:
                      type: string
                      format: uuid
                      description: Rechnung, aus der der Nachweis entsteht (Bezugsrechnung).
                    kind:
                      type: string
                      enum:
                        - proof
                        - progress
                      default: proof
                    title:
                      type:
                        - string
                        - "null"
                      maxLength: 120
                      description: Eigener Titel; leer = Label der Art.
                    customer_id:
                      type: string
                      format: uuid
                      description: Optional; muss dann zum Kunden der Rechnung passen (422 sonst).
      responses:
        "201":
          description: |
            Nachweis angelegt. Bei `from_invoice_id` zusätzlich `invoice_link` (die Verknüpfung als Anlage oder null).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/Statement"
                      - type: object
                        properties:
                          invoice_link:
                            description: Nur bei `from_invoice_id` — Verknüpfung als Anlage, sonst null.
                            oneOf:
                              - $ref: "#/components/schemas/InvoiceStatementLink"
                              - type: "null"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Die gewünschte `statement_number` ist im Workspace bereits vergeben.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /statements/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - statements
      operationId: getStatement
      summary: Kopf eines Nachweises samt aufgelöster Bausteinkonfiguration
      description: |
        Der Nachweis ohne Snapshot, dazu `config` — die robust aufgelöste Bausteinkonfiguration (`content_config`; unbekannte Schlüssel fallen weg, ungültige Werte auf den Standard, ein leeres `content_config` fällt auf den alten `scope` zurück) — und der Kundenname. Das fertige Dokument liefert `GET /statements/{id}/document`, die Auswahlgrundlage `GET /statements/{id}/candidates`.
      responses:
        "200":
          description: Nachweis mit `config` und `customer_name`.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/Statement"
                      - type: object
                        required:
                          - config
                          - customer_name
                        properties:
                          config:
                            $ref: "#/components/schemas/StatementContentConfig"
                          customer_name:
                            type:
                              - string
                              - "null"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - statements
      operationId: updateStatement
      summary: Kopf eines Nachweises ändern, zurücksetzen oder archivieren
      description: |
        Ändert die übergebenen Felder (PATCH-Semantik). Inhaltliche Änderungen sind nur am ENTWURF möglich (409 sonst). Bausteine, Auswahl und Darstellung ändert `PUT /statements/{id}/content` — `scope` und `layout` sind hier nicht mehr schreibbar (das Layout kommt aus der Vorlage).
        `statement_number` setzt eine eigene Nummer: sie muss im Workspace frei sein (409, auch gegen archivierte Nachweise), die Form `ENTWURF-…` ist reserviert (422); null oder "" setzt wieder einen Platzhalter, dann vergibt das Festschreiben die Nummer aus dem Nummernkreis. Trägt der Nachweis schon eine Nummer, die der Nummernkreis vergeben hat (etwa nach dem Zurücksetzen auf Entwurf), bleibt sie: sie zu ersetzen oder zu entfernen endet mit 409 — sonst entstünde eine Lücke im Nummernkreis.
        `wizard_step` merkt sich den zuletzt geöffneten Schritt des Assistenten (`rahmen`, `inhalte`, `pruefen`); er ist Bedienzustand und auch am festgeschriebenen Nachweis schreibbar.
        `status: 'draft'` setzt einen FESTGESCHRIEBENEN Nachweis zurück: Snapshot, Layout-Schnappschuss, Ausstellungsdatum und das abgelegte PDF verfallen, die vergebene Nummer bleibt (ein Nachweis ist kein Steuerbeleg). An einem Entwurf ist es wirkungslos (422, wenn sonst nichts geändert wird). Festgeschrieben wird ausschließlich über `POST /statements/{id}/finalize`; `status: 'final'` hier ist ein 422. `archived` ist kein Inhaltswechsel und deshalb auch am festgeschriebenen Nachweis erlaubt.
        Das ZURÜCKSETZEN endet mit 409 (`details.invoice_numbers`), solange der Nachweis einer GESTELLTEN Rechnung als Anlage beiliegt: die Rechnung hängt ihn bei jedem Download an und sähe sonst anders aus als die, die beim Kunden liegt. Ein verworfener Rechnungsentwurf (ohne echte Nummer) zählt nicht.
        Ein KUNDENWECHSEL (`customer_id`) endet mit 409, solange der Nachweis als Anlage an einer Rechnung hängt: die Anlage zeigt die Leistungen ihres Kunden, und sie darf nicht nachträglich zu denen eines anderen werden — das wäre eine Offenlegung gegenüber dem Rechnungsempfänger. Erst die Verknüpfung lösen, dann umhängen.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/StatementInput"
                - type: object
                  properties:
                    status:
                      type: string
                      enum:
                        - draft
                      description: Nur `draft` — Zurücksetzen eines festgeschriebenen Nachweises.
                    archived:
                      type: boolean
      responses:
        "200":
          description: Nachweis geändert (ohne Snapshot).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Statement"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Nachweis ist festgeschrieben und inhaltlich gesperrt, die `statement_number` ist bereits vergeben (`details.field = statement_number`) — oder der Kunde soll gewechselt werden, während der Nachweis einer Rechnung als Anlage beiliegt (`details.invoice_numbers`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - statements
      operationId: archiveStatement
      summary: Nachweis archivieren (Soft-Delete) oder Entwurf löschen
      description: |
        Standard ist das Archivieren: Ein festgeschriebener Nachweis wird nie hart gelöscht — seine Nummer bleibt vergeben und das Dokument nachvollziehbar. Bestehende Verknüpfungen zu Rechnungen bleiben bestehen. Mit `hard=true` wird ein ENTWURF (auch ein archivierter) endgültig gelöscht; seine Verknüpfungen zu Rechnungsentwürfen fallen mit weg. Ein festgeschriebener Nachweis oder ein Entwurf an einer gestellten Rechnung ergibt 409 — ebenso ein Entwurf, der schon eine Nummer aus dem Nummernkreis trägt (z. B. nach dem Zurücksetzen auf Entwurf): sein Löschen risse eine Lücke in den Kreis. Archivieren bleibt möglich.
      parameters:
        - name: hard
          in: query
          required: false
          description: |
            `true` löscht einen Entwurf endgültig statt ihn zu archivieren. Alles andere (auch das Weglassen) archiviert.
          schema:
            type: string
            enum:
              - "true"
      responses:
        "200":
          description: "Archiviert (`archived: true`) bzw. gelöscht (`deleted: true`)."
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      archived:
                        type: boolean
                      deleted:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /statements/{id}/candidates:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - statements
      operationId: getStatementCandidates
      summary: Auswahlgrundlage eines Nachweises (Leistungen im Rahmen)
      description: |
        Alles, woraus der Nachweis wählen kann: Kunde, seine Projekte mit Stunden und Sessions im Zeitraum, Zeiteinträge (mit Uhrzeiten und wirksamem Stundensatz), KI-Sessions und Projektkosten der gewählten Projekte, Commits (nur wenn der Baustein `commits` aktiv ist oder `?commits=true`), die Bezugsrechnung samt ihrer Leistungs-IDs, der Auftrag und das Ende des letzten festgeschriebenen Nachweises dieses Kunden (Vorschlag „Seit letztem Nachweis“). `billedOn` ist rein informativ — ein Nachweis sperrt nichts. Aus Kandidaten und `config` rechnet der Assistent mit derselben reinen Funktion wie der Server die Live-Gliederung (`StatementBody`).
      parameters:
        - name: commits
          in: query
          required: false
          description: "`true` lädt die Commits auch ohne aktiven Baustein (Vorschau beim Hinzufügen)."
          schema:
            type: string
            enum:
              - "true"
              - "false"
      responses:
        "200":
          description: Auswahlgrundlage.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/StatementCandidates"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
  /statements/{id}/content:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    put:
      tags:
        - statements
      operationId: updateStatementContent
      summary: Bausteine, Auswahl und Darstellung eines Entwurfs speichern
      description: |
        Speichert `config` (`StatementContentConfig`) als `content_config`. Die Konfiguration wird robust aufgelöst — unbekannte Schlüssel fallen weg, ungültige Werte auf den Standard — und ERSETZT die gespeicherte vollständig (vorher per `GET /statements/{id}` lesen). Die Antwort trägt die gespeicherte Konfiguration und den daraus auf frisch geladenen Kandidaten gebauten Inhalt (`body`): was dort steht, zeigt das Dokument. Nur am Entwurf (409, auch wenn der Nachweis zwischenzeitlich festgeschrieben wurde).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - config
              properties:
                config:
                  $ref: "#/components/schemas/StatementContentConfig"
      responses:
        "200":
          description: Gespeicherte Konfiguration und gebauter Inhalt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatementContentResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Nachweis ist festgeschrieben.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /statements/{id}/detach:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - statements
      operationId: detachStatementSelection
      summary: Auswahl von der Rechnung lösen
      description: |
        Ein aus einer Rechnung erzeugter Nachweis folgt deren Auswahl (`selection.source = invoice`). Lösen schreibt die AKTUELLE Auswahl als gleichwertige eigene fest (`source = own`) — das Dokument sieht danach exakt so aus wie vorher, folgt späteren Änderungen der Rechnung aber nicht mehr. Der Bezug (`source_invoice_id`) und eine Verknüpfung als Anlage bleiben bestehen. Nur am Entwurf, und nicht, solange der Entwurf einer gestellten Rechnung beiliegt (409 — Altfall, der Nachweis wird dann festgeschrieben). Nicht zu verwechseln mit `DELETE /invoices/{id}/statements/{statementId}` (löst die Anlage-Verknüpfung; an einer gestellten Rechnung ebenfalls 409).
      responses:
        "200":
          description: Gelöste Konfiguration und gebauter Inhalt (wie `PUT …/content`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatementContentResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Nachweis ist festgeschrieben oder folgt keiner Rechnung.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /statements/{id}/progress:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - statements
      operationId: refreshStatementProgress
      summary: Fortschritt aus den Projekten übernehmen bzw. aktualisieren
      description: "Baustein „Fortschritt“ (`progress`): kopiert die Fortschrittspunkte der Projekte des Nachweises für seinen Zeitraum nach `content_config.progress` — als Wochenpunkte (`source: weeks`, ohne KI) oder je Projekt als verdichteten Bericht (`source: report`, wie `POST /projects/{id}/progress/report`, wiederverwendet bei unveränderten Wochenpunkten; `fresh: true` erzwingt neue Berichte). Ohne `source` gilt die im Nachweis gewählte Quelle. Ersetzt die bisherige Kopie samt Bearbeitungen und setzt den Baustein in den Nachweis. Die Kopie schreibt nie ins Projekt zurück. Nur am Entwurf; bei ausgeschaltetem Projektfortschritt 409 `progress_disabled`."
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                source:
                  type: string
                  enum:
                    - weeks
                    - report
                fresh:
                  type: boolean
      responses:
        "200":
          description: Gespeicherte Konfiguration, Zahl der Projekte und Punkte.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - config
                      - projects
                      - points
                    properties:
                      config:
                        $ref: "#/components/schemas/StatementContentConfig"
                      projects:
                        type: integer
                      points:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /statements/{id}/document:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - statements
      operationId: getStatementDocument
      summary: Fertiges Nachweisdokument als HTML
      description: |
        Das Dokument im Nachweis-Layout der Vorlage (ohne eigenes Nachweis-Layout: das der Rechnung). Ein ENTWURF wird live gerendert, ein FESTGESCHRIEBENER Nachweis aus seinem Snapshot samt eingefrorenem Layout — ein Dokument, das beim Kunden liegt, ändert sich nicht rückwirkend. `mode=print` ist genau das HTML, aus dem das PDF entsteht. Nie zwischengespeichert (`cache-control: no-store`).
      parameters:
        - name: mode
          in: query
          required: false
          schema:
            type: string
            enum:
              - screen
              - print
            default: screen
      responses:
        "200":
          description: HTML-Dokument.
          content:
            text/html:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /statements/{id}/finalize:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - statements
      operationId: finalizeStatement
      summary: Nachweis festschreiben (Nummer, Snapshot, PDF)
      description: |
        Friert ein: die Nummer (aus dem Nachweis-Nummernkreis der Vorlage, falls noch ein Platzhalter), Absender und Firma aus der Rechnungsvorlage (`sender_*`, `signer_name`, `greeting_text`), Einleitung/Schlusstext (eigener Text vor Vorlage), Empfänger, Layout (ein eigenes Layout samt Quelltext), Ausstellungsdatum und den fertig gegliederten Inhalt (`snapshot`, Version 2). Danach rendert der Nachweis aus dem Snapshot. Das PDF entsteht nach der Antwort und liegt dann im Speicher (`pdf_path`); scheitert es, holt der nächste Download es nach.
        Ohne Baustein → 422. Erneutes Festschreiben antwortet 409 — es würde ein bereits erteiltes Dokument stillschweigend gegen ein neues tauschen; zwei gleichzeitige Anfragen entscheidet die Datenbank. Scheitert das Festschreiben nach der Nummernvergabe (409, 500), geht die vergebene Nummer an den Nummernkreis zurück. Zum Neuaufbau erst über `PATCH { "status": "draft" }` zurücksetzen (nicht an einer gestellten Rechnung). Angehängte Nachweis-Entwürfe schreibt auch das Stellen einer Rechnung fest (`PATCH /invoices/{id}`); dieser Endpunkt bleibt der Weg für eigenständige Nachweise und für Altfälle, deren Rechnung schon gestellt ist.
      responses:
        "200":
          description: Nachweis festgeschrieben (ohne Snapshot).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Statement"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Nachweis ist bereits festgeschrieben, oder die Nummer ist bereits vergeben.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /statements/{id}/export:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - statements
      operationId: exportStatement
      summary: Nachweis als Datei (PDF/Excel/CSV)
      description: |
        `pdf` ist dasselbe Dokument wie `GET /statements/{id}/document` im Layout der Vorlage; ein festgeschriebener Nachweis liefert das beim Festschreiben abgelegte PDF (fehlt es, wird es aus dem Snapshot erzeugt und nachgelegt). `xlsx`/`csv` enthalten den gegliederten Inhalt flach als Tabellen (Gruppenzeilen, Zwischensummen, Summe; Zahlen als echte Zahlen). Läuft durch dasselbe Pro-Gate wie `/export`.
        Ein Nachweis wird AUSSCHLIESSLICH als Datei geteilt: es gibt bewusst keinen Share-Link und keinen öffentlichen Token, sonst läge detaillierte Kundenarbeitszeit hinter einer URL, die jeder weiterreichen kann. Deshalb nur der eingeloggte Workspace-Kontext.
      parameters:
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - pdf
              - xlsx
              - csv
            default: pdf
      responses:
        "200":
          description: Datei-Download (Content-Disposition attachment).
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            text/csv:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoice-template:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoice-design
      operationId: getInvoiceTemplate
      summary: Rechnungsvorlage lesen
      description: Absender, Steuer-IDs, Handelsregister, Bank, Nummernkreis, Standardtexte und Layout. Ohne gespeicherte Zeile kommen Standardwerte — mit dem Firmennamen aus dem Branding als Absender und einem Nummernkreis-Startwert über den bisherigen Nummern. `meta.stored` sagt, ob schon etwas gespeichert ist. Alle Rollen.
      responses:
        "200":
          description: Vorlage.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoiceTemplateResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    put:
      tags:
        - invoice-design
      operationId: updateInvoiceTemplate
      summary: Rechnungsvorlage speichern (Nummernkreis, Texte, Bank, Layout)
      description: "Speichert die Vorlage (upsert). Fehlende Felder bleiben wie gespeichert — AUSSER `layout`: wird es gesendet, ersetzt es das gespeicherte Layout vollständig. Geprüft werden u. a. IBAN (Prüfsumme), BIC, E-Mail, Präfix, Stellenzahl und der Steuersatz. `numberYear` setzt nur der Server; ändert sich die nächste Nummer oder das Format, gehört der Zähler ab dann zum laufenden Jahr."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceTemplate"
      responses:
        "200":
          description: Gespeicherte Vorlage (`meta.stored` = true).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoiceTemplateResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoice-layouts:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoice-design
      operationId: listInvoiceLayouts
      summary: Eigene Layouts auflisten (ohne Quelltext)
      description: Nach Namen sortiert. Ohne `document_type` kommen die Layouts beider Dokumentarten (Rechnung und Leistungsnachweis).
      parameters:
        - name: document_type
          in: query
          required: false
          description: Nur Layouts dieser Dokumentart (unbekannter Wert → 422).
          schema:
            type: string
            enum:
              - invoice
              - statement
      responses:
        "200":
          description: Layouts.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/InvoiceLayoutSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    post:
      tags:
        - invoice-design
      operationId: createInvoiceLayout
      summary: Eigenes Layout anlegen (Rechnung oder Leistungsnachweis)
      description: "HTML mit Liquid-Platzhaltern plus CSS. `documentType` (Standard `invoice`) legt die Dokumentart fest und lässt sich danach nicht ändern — die Platzhalter beider Arten unterscheiden sich. Ein Rechnungslayout verwendet die Vorlage über `layout.design: eigenes` + `layout.customLayoutId` (oder je Rechnung), ein Nachweis-Layout über `statementLayout`."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/InvoiceLayoutInput"
                - type: object
                  required:
                    - name
      responses:
        "201":
          description: Layout angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceLayout"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoice-layouts/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - invoice-design
      operationId: getInvoiceLayout
      summary: Eigenes Layout samt Quelltext lesen
      responses:
        "200":
          description: Layout.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceLayout"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    patch:
      tags:
        - invoice-design
      operationId: updateInvoiceLayout
      summary: Eigenes Layout ändern
      description: Jede tatsächliche Änderung am Quelltext zählt `version` hoch. Bereits gestellte Rechnungen und festgeschriebene Nachweise behalten ihren eingefrorenen Quelltext. Ohne Änderung kommt das Layout unverändert zurück. Eine abweichende `documentType` ist ein 422 (die Art eines Layouts ist fest).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceLayoutInput"
      responses:
        "200":
          description: Aktualisiertes Layout.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceLayout"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - invoice-design
      operationId: deleteInvoiceLayout
      summary: Eigenes Layout löschen (endgültig)
      description: Erlaubt, auch wenn gestellte Rechnungen oder festgeschriebene Nachweise es nutzten (sie tragen einen Schnappschuss). Nutzt die Vorlage es gerade — als Rechnungslayout (`layout`) oder als Nachweis-Layout (`statementLayout`) → 409.
      responses:
        "200":
          $ref: "#/components/responses/Deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Die Vorlage nutzt dieses Layout als Rechnungs- oder Nachweis-Layout (`conflict`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /invoice-layouts/pdf:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - invoice-design
      operationId: previewInvoiceLayoutPdf
      summary: Probe-PDF eines (ungespeicherten) eigenen Layouts
      description: "Setzt Beispieldaten mit den Angaben der Rechnungsvorlage und dem Branding in das übergebene Layout und liefert das PDF, wie Chromium es rendert (Seitenumbrüche, Fußzeile, Schriften): `documentType: invoice` (Standard) mit der Beispielrechnung, `statement` mit einem Beispiel-Leistungsnachweis (Kapitel, Gruppen, Zwischensummen, Unterschriftsfeld). Speichert nichts. Ein Liquid-Fehler im Quelltext → 422 (`field: sourceHtml`). Schreibrolle nötig."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sourceHtml:
                  type: string
                  maxLength: 200000
                sourceCss:
                  type: string
                  maxLength: 200000
                name:
                  type: string
                  maxLength: 120
                  description: Wird geprüft, aber nicht verwendet.
                documentType:
                  type: string
                  enum:
                    - invoice
                    - statement
                  default: invoice
                  description: Beispieldokument der Probe (auch `document_type`).
      responses:
        "200":
          description: PDF (Content-Disposition inline, probe-rechnungslayout.pdf bzw. probe-nachweislayout.pdf).
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /workspace/branding:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - branding
      operationId: getWorkspaceBranding
      summary: Corporate-Design des Workspaces lesen
      description: Firmenname, Akzentfarbe, Fußzeilentext und ob ein Logo hinterlegt ist — erscheint auf Exporten, Nachweisen und (sofern das Rechnungslayout nichts anderes setzt) Rechnungen. Ohne Konfiguration kommen leere Werte. Alle Rollen.
      responses:
        "200":
          description: Aktuelle Branding-Konfiguration.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/WorkspaceBranding"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    put:
      tags:
        - branding
      operationId: updateWorkspaceBranding
      summary: Corporate-Design des Workspaces speichern
      description: Speichert ALLE drei Textfelder zugleich (upsert) — ein nicht gesendetes oder leeres Feld wird geleert. Daher zum Ändern eines einzelnen Werts erst lesen und alle Felder senden. Das Logo bleibt unberührt (eigene Route).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                company_name:
                  type:
                    - string
                    - "null"
                  maxLength: 120
                accent_color:
                  type:
                    - string
                    - "null"
                  pattern: ^#[0-9a-fA-F]{6}$
                footer_text:
                  type:
                    - string
                    - "null"
                  maxLength: 300
      responses:
        "200":
          description: Gespeicherte Branding-Konfiguration.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/WorkspaceBranding"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /workspace/branding/logo:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - branding
      operationId: getWorkspaceLogo
      summary: Workspace-Logo herunterladen
      responses:
        "200":
          description: Logo-Bytes.
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
    post:
      tags:
        - branding
      operationId: uploadWorkspaceLogo
      summary: Workspace-Logo hochladen
      description: multipart/form-data mit dem Feld `logo` (PNG oder JPEG, höchstens 2 MB, serverseitig geprüft). Ersetzt ein vorhandenes Logo und entfernt das alte aus dem Speicher.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - logo
              properties:
                logo:
                  type: string
                  format: binary
                  description: PNG/JPEG, ≤ 2.097.152 Bytes.
            encoding:
              logo:
                contentType: image/png, image/jpeg
      responses:
        "200":
          description: "Branding mit `has_logo: true`."
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/WorkspaceBranding"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          description: Upload in den Speicher fehlgeschlagen (`internal_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags:
        - branding
      operationId: deleteWorkspaceLogo
      summary: Workspace-Logo entfernen
      responses:
        "200":
          description: "Branding mit `has_logo: false`."
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/WorkspaceBranding"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /share-card:
    get:
      tags:
        - share
      operationId: getShareCard
      summary: Eigene Share-Karte und Live-Statistik lesen
      description: Liefert die eigene Karte (oder null) plus die aggregierte Live-Statistik (unabhängig von `enabled`).
      responses:
        "200":
          description: Eigene Karte (oder null) und aggregierte Statistik.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - card
                      - stats
                    properties:
                      card:
                        oneOf:
                          - $ref: "#/components/schemas/ShareCard"
                          - type: "null"
                      stats:
                        $ref: "#/components/schemas/ShareCardStats"
        "401":
          $ref: "#/components/responses/Unauthorized"
    put:
      tags:
        - share
      operationId: upsertShareCard
      summary: Eigene Share-Karte anlegen oder ändern (Upsert)
      description: |
        Anlegen (noch keine Karte): `display_name` Pflicht (1–80), das Token wird serverseitig erzeugt, `enabled` optional (Standard false). Ändern: alle Felder optional; ohne Änderung kommt die Karte unverändert (200) zurück.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name:
                  type: string
                  minLength: 1
                  maxLength: 80
                enabled:
                  type: boolean
                use_profile_avatar:
                  type: boolean
                  description: Profilfoto statt eigenem Avatar zeigen (nur beim Ändern).
      responses:
        "200":
          description: Karte geändert.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ShareCard"
        "201":
          description: Karte angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ShareCard"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - share
      operationId: deleteShareCard
      summary: Eigene Share-Karte löschen (inkl. Avatar)
      responses:
        "200":
          description: Karte gelöscht.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - deleted
                    properties:
                      deleted:
                        type: boolean
                        const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
  /share-card/rotate:
    post:
      tags:
        - share
      operationId: rotateShareCard
      summary: Token der Share-Karte neu erzeugen
      description: Erzeugt ein neues Token; bereits verteilte Links und Einbettungen werden dadurch ungültig.
      responses:
        "200":
          description: Neues Token gesetzt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ShareCard"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "Es existiert noch keine Karte (code: no_card)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /share-card/avatar:
    post:
      tags:
        - share
      operationId: uploadShareCardAvatar
      summary: Avatar der Share-Karte hochladen
      description: |
        multipart/form-data mit dem Feld file (image/jpeg oder image/png, höchstens 512 KB; serverseitig geprüft). Ersetzt einen vorhandenen Avatar und entfernt den alten aus dem Speicher.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Avatar aktualisiert.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/ShareCard"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "Es existiert noch keine Karte (code: no_card)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
  /support-requests:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - support
      operationId: listSupportRequests
      summary: Eigene Support-Anfragen und Feedback auflisten
      description: Die jüngsten 200 Anfragen des Workspace, neueste zuerst.
      responses:
        "200":
          description: Anfragen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SupportRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    post:
      tags:
        - support
      operationId: createSupportRequest
      summary: Support-Anfrage stellen oder Feedback geben
      description: Legt die Anfrage samt erster Nachricht an und benachrichtigt das Support-Team.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
                - subject
                - message
              properties:
                type:
                  type: string
                  enum:
                    - support
                    - feedback
                subject:
                  type: string
                  minLength: 1
                  maxLength: 200
                message:
                  type: string
                  minLength: 1
                  maxLength: 10000
      responses:
        "201":
          description: Anfrage angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/SupportRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /support-requests/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - support
      operationId: getSupportRequest
      summary: Support-Anfrage samt Nachrichtenverlauf lesen
      responses:
        "200":
          description: Anfrage mit Nachrichten (älteste zuerst).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/SupportRequest"
                      - type: object
                        required:
                          - messages
                        properties:
                          messages:
                            type: array
                            items:
                              $ref: "#/components/schemas/SupportMessage"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /support-requests/{id}/messages:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - support
      operationId: replySupportRequest
      summary: Auf eine Support-Anfrage antworten
      description: Fügt eine eigene Nachricht an. Eine beantwortete oder geschlossene Anfrage wird dadurch wieder `open`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - message
              properties:
                message:
                  type: string
                  minLength: 1
                  maxLength: 10000
      responses:
        "201":
          description: Nachricht angelegt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/SupportMessage"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /github/connect:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - github
      operationId: githubConnect
      summary: Signierte GitHub-Install-URL erzeugen
      description: |
        Liefert die URL zum Installieren der GitHub App inkl. eines signierten `state`-Parameters (CSRF-/Claim-Bindung an Workspace + Nutzer). Der Client navigiert dorthin. Nur Rolle owner/member (Viewer → 403). Solange die GitHub App nicht konfiguriert ist → 503.
      responses:
        "200":
          description: Signierte Install-URL.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - url
                    properties:
                      url:
                        type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Nur Owner/Member dürfen die Integration verbinden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: "Die GitHub-Integration ist nicht konfiguriert (code: github_not_configured)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /github/installations:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - github
      operationId: listGithubInstallations
      summary: Verbundene GitHub-Installationen auflisten
      description: |
        Liefert die verbundenen GitHub-Accounts/Orgs des Workspace (Login, Typ, Avatar, Status). Alle Workspace-Rollen.
      responses:
        "200":
          description: Liste der Installationen.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/GithubInstallation"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /github/installations/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    delete:
      tags:
        - github
      operationId: disconnectGithubInstallation
      summary: GitHub-Installation trennen
      description: |
        Trennt die Verbindung: best-effort Deinstallation bei GitHub, danach lokaler Soft-Delete der Installation und ihrer Repositories. GitHub-eigene Commit-Zeilen werden soft-gelöscht; angereicherte Collector-Zeilen behalten ihre Statistiken (nur die Repo-Zuordnung entfällt). Nur Rolle owner/member (Viewer → 403).
      responses:
        "200":
          description: Verbindung getrennt.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /github/installations/{id}/repositories:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - github
      operationId: listGithubInstallationRepositories
      summary: Live-Repository-Liste einer Installation
      description: |
        Liefert die aktuell für die Installation sichtbaren Repositories direkt von GitHub, jede Zeile annotiert mit `linked_project_id`/`linked_project_name`, falls sie im Workspace bereits verknüpft ist. Solange die GitHub App nicht konfiguriert ist → 503.
      responses:
        "200":
          description: Live-Repository-Liste.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/GithubInstallationRepo"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /github/repositories:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - github
      operationId: listGithubRepositories
      summary: Verknüpfte Repositories auflisten
      description: |
        Liefert die mit Projekten verknüpften Repositories des Workspace, optional gefiltert per `project_id`. Enthält den Sync-Zustand (`sync_status`, `sync_error`, `last_synced_at`).
      parameters:
        - in: query
          name: project_id
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Liste der verknüpften Repositories.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/GithubRepository"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - github
      operationId: linkGithubRepository
      summary: Repository mit einem Projekt verknüpfen
      description: |
        Verknüpft ein GitHub-Repository mit einem Projekt. Die Repo-Metadaten werden NICHT vom Client übernommen, sondern serverseitig gegen GitHub verifiziert. Der Initial-Sync startet entkoppelt im Hintergrund. Nur Rolle owner/member (Viewer → 403). Doppelte Verknüpfung → 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - installation_id
                - project_id
                - github_repo_id
              properties:
                installation_id:
                  type: string
                  format: uuid
                  description: ID der Installations-Zeile (nicht die GitHub-installation_id).
                project_id:
                  type: string
                  format: uuid
                github_repo_id:
                  type: integer
                  format: int64
                  description: GitHub `databaseId` des Repositories (stabil bei Rename/Transfer).
      responses:
        "201":
          description: Repository verknüpft (Initial-Sync läuft im Hintergrund an).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/GithubRepository"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Nur Owner/Member dürfen Repositories verknüpfen.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: "Das Repository ist bereits einem Projekt zugeordnet (code: conflict)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationError"
        "503":
          description: "Die GitHub-Integration ist nicht konfiguriert (code: github_not_configured)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /github/repositories/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    delete:
      tags:
        - github
      operationId: unlinkGithubRepository
      summary: Repository-Verknüpfung lösen
      description: |
        Löst die Verknüpfung: Repo-Zeile soft-delete, GitHub-eigene Commits soft-delete; angereicherte Collector-Zeilen behalten ihre Statistiken (nur die Repo-Zuordnung entfällt). Nur Rolle owner/member (Viewer → 403).
      responses:
        "200":
          description: Verknüpfung gelöst.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /github/repositories/{id}/sync:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/WorkspaceHeader"
    post:
      tags:
        - github
      operationId: syncGithubRepository
      summary: Repository jetzt synchronisieren
      description: |
        Stößt einen sofortigen, inline laufenden Sync des Repositories an und liefert die aktualisierte Repo-Zeile zurück. Läuft bereits ein frischer Sync → 409. Nur Rolle owner/member (Viewer → 403). Solange die GitHub App nicht konfiguriert ist → 503.
      responses:
        "200":
          description: Sync abgeschlossen (aktualisierte Repo-Zeile).
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/GithubRepository"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "Für dieses Repository läuft bereits eine Synchronisierung (code: conflict)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: "Die GitHub-Integration ist nicht konfiguriert (code: github_not_configured)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /streamdeck/download:
    get:
      tags:
        - downloads
      operationId: downloadStreamdeckPlugin
      summary: Stream-Deck-Plugin als installierbare Datei herunterladen
      description: |
        Liefert das gebündelte Stream-Deck-Plugin (.streamDeckPlugin) als Download. Ein Doppelklick auf die Datei installiert das Plugin in der Stream-Deck-App (ab Version 7.1). Auth: Session, Bearer-JWT ODER nutzergebundener API-Key (Bearer tmk_… / X-API-Key) — bewusst autorisiert statt öffentlich. Kein/ungültiger Key UND keine Session → 401. Der Key-Pfad ist read-only und rate-limitiert. Die Antwort trägt Version + SHA-256 als Header zur Verifikation.
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      responses:
        "200":
          description: Plugin-Download (Content-Disposition attachment).
          headers:
            X-Streamdeck-Plugin-Version:
              description: Version des gebündelten Plugins (aus manifest.json).
              schema:
                type: string
            X-Streamdeck-Plugin-SHA256:
              description: SHA-256 (hex) der Plugin-Datei zur Integritätsprüfung.
              schema:
                type: string
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /limits:
    get:
      tags:
        - limits
      operationId: listLimits
      summary: Eigene Abo-Limits der KI-Werkzeuge
      description: "Letzter gemeldeter Stand der eigenen Abo-Limits je Anbieter und Konto (Claude: 5-Stunden- und Wochenfenster samt modellbezogener Wochenfenster und Extra Usage; Codex: Fenster und Credits). Sortiert nach Anbieter, dann neuester Stand zuerst. `stale` ist true, wenn kein erfolgreicher Abruf vorliegt, er älter als 15 Minuten ist oder der letzte Abruf fehlschlug. Fenster mit `resets_at` in der Vergangenheit sind inzwischen zurückgesetzt. Die Daten stammen aus inoffiziellen Schnittstellen der Anbieter und können ausfallen. Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert; auch ohne Pro)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: provider
          in: query
          required: false
          description: Nur diesen Anbieter.
          schema:
            type: string
            enum:
              - claude
              - codex
      responses:
        "200":
          description: Abo-Limits des Nutzers.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/LimitSnapshot"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /statusline:
    parameters:
      - $ref: "#/components/parameters/WorkspaceHeader"
    get:
      tags:
        - reporting
      operationId: getStatusline
      summary: Projekt, Aufwand heute und Auftragsstand für die Statuszeile
      description: "Eine Anfrage für die Statuszeile eines KI-Tools (`tm statusline`). Das Projekt kommt aus der KI-Session (`session_id` = Sitzungskennung des Tools, wie sie der Kollektor als `client_id` meldet), sonst aus den Ordner- und Repository-Regeln für `cwd` bzw. `repo` (wie die Zuordnung beim Ingest, einschließlich Auffangregeln). `today` summiert den laufenden Tag (Europe/Berlin) wie die Hauptauswertung. `order` ist der offene Auftrag mit Kontingent, in den Arbeit an diesem Projekt heute läuft (erster noch nicht voller in der Reihenfolge der Auftragsbindung, sonst der letzte); `percent` = Geleistetes in Prozent des Volumens bzw. Zeitkontingents. Tarifgrenzen: ohne Rentabilität ist `today.value_micros` null, ohne Aufträge ist `order` null. Ohne Treffer stehen alle Felder auf null. Auth: Session, Bearer-JWT ODER API-Key (rate-limitiert; auch ohne Pro)."
      security:
        - cookieAuth: []
        - bearerJwt: []
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: session_id
          in: query
          required: false
          description: Sitzungskennung des Tools (UUID), etwa `session_id` der Claude-Code-Statuszeile.
          schema:
            type: string
            format: uuid
        - name: cwd
          in: query
          required: false
          description: Arbeitsordner (absoluter Pfad) für die Ordnerregeln.
          schema:
            type: string
            maxLength: 1024
        - name: repo
          in: query
          required: false
          description: Repository als `host/pfad` (z. B. `github.com/org/repo`) für Repository-Regeln.
          schema:
            type: string
            maxLength: 300
      responses:
        "200":
          description: Stand für die Statuszeile.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Statusline"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: sb-access-token
      description: Supabase-Session-Cookie der interaktiven Web-App.
    bearerJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Bearer-JWT aus dem Token-Tausch (`POST /auth/token`): kurzlebiges Supabase-Access-Token des Key-Inhabers als "Authorization: Bearer <JWT>" (kein `tmk_`-Präfix). Die Anfrage läuft damit exakt wie eine Web-Session — RLS, Rollen, RPCs und Trigger gelten unverändert, auf jeder Route. Laufzeit Standard 1 h, kein Refresh-Token: nach Ablauf oder bei 401 neu tauschen. Wird der Schreib-Key widerrufen, ist das Token sofort ungültig. Der Header `X-TM-Workspace` wählt den Workspace (sonst der älteste eigene).'
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Nutzergebundener API-Key als "Authorization: Bearer tmk_<key>". Direkt verwendbar für den Ingest (POST /ingest/events), die Lese-Routen der Daten-API und die Timer-Schreibrouten der lokalen Clients; überall sonst nur über den Token-Tausch (`bearerJwt`). Der Workspace stammt ausschließlich aus dem Key. Zugriffe sind pro Key rate-limitiert (Fixed-Window; RateLimit-Limit/-Remaining/-Reset-Header, bei Überschreitung 429 mit Retry-After).'
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Nutzergebundener API-Key als "X-API-Key: tmk_<key>" (Alternative zu `bearerAuth`, gleiche Semantik und Rate-Limits).'
    cronSecret:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer <CRON_SECRET>` des Vercel-Crons (nur Betrieb)."
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: Seitengröße (Standard 20, höchstens 100; größere Werte werden gekappt).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Cursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
      description: Opaker Keyset-Cursor aus `next_cursor` der vorigen Seite. Ungültig → 400 `invalid_cursor`.
    PathId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Range:
      name: range
      in: query
      required: false
      description: Zeitraum-Preset (`month` = laufender Kalendermonat). Wird von `from`/`to` übersteuert, wo vorhanden.
      schema:
        type: string
        enum:
          - 7d
          - 30d
          - month
          - all
        default: 30d
    FromDay:
      name: from
      in: query
      required: false
      description: Untere Grenze als ISO-Kalendertag (YYYY-MM-DD, inklusive). Nur zusammen mit `to`; übersteuert `range`.
      schema:
        type: string
        format: date
    ToDay:
      name: to
      in: query
      required: false
      description: Obere Grenze als ISO-Kalendertag (YYYY-MM-DD, inklusive). Nur zusammen mit `from`; nicht vor `from`.
      schema:
        type: string
        format: date
    WorkspaceHeader:
      name: X-TM-Workspace
      in: header
      required: false
      description: Wählt bei Anmeldung per `bearerJwt` den Workspace unter den eigenen Mitgliedschaften (UUID). Ohne Header gilt der älteste eigene Workspace. Kein Mitglied → 403. Bei API-Keys wirkungslos (der Workspace stammt aus dem Key).
      schema:
        type: string
        format: uuid
  headers:
    RateLimitLimit:
      description: Maximale Anfragen je Fenster (je Key).
      schema:
        type: integer
    RateLimitRemaining:
      description: Verbleibende Anfragen im aktuellen Fenster.
      schema:
        type: integer
    RateLimitReset:
      description: Sekunden bis zum Fenster-Reset.
      schema:
        type: integer
    RetryAfter:
      description: Sekunden bis ein erneuter Versuch erlaubt ist (bei 429).
      schema:
        type: integer
    ContentDisposition:
      description: Dateiname des Downloads (`attachment` bzw. `inline`).
      schema:
        type: string
  responses:
    Deleted:
      description: Gelöscht bzw. archiviert.
      content:
        application/json:
          schema:
            type: object
            required:
              - data
            properties:
              data:
                type: object
                required:
                  - id
                properties:
                  id:
                    type: string
                    format: uuid
    Archived:
      description: Archiviert (Soft-Delete, Wiederherstellen per PATCH `archived:false`).
      content:
        application/json:
          schema:
            type: object
            required:
              - data
            properties:
              data:
                type: object
                required:
                  - id
                  - archived
                properties:
                  id:
                    type: string
                    format: uuid
                  archived:
                    type: boolean
                    const: true
    BadRequest:
      description: Ungültiger Cursor (`invalid_cursor`) oder fehlerhafte Anfrage.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Keine gültige Session, kein gültiges Token bzw. ungültiger API-Key (`unauthorized`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    PaymentRequired:
      description: "Pro-Funktion ohne Pro-Umfang (`upgrade_required`, nur bei eingeschalteten Tarifgrenzen). `details`: `{ feature, upgrade_url }` — `feature` ist einer von `invoices`, `orders`, `payments`, `statements`, `profitability`, `export`, `api`, `ai_history`, `progress_report`; `upgrade_url` zeigt auf die Tarifseite (`/app/billing`)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Keine Berechtigung (`forbidden`) — Rolle ohne Schreibrecht (viewer), Read-Key auf einer Schreibroute oder widerrufener Key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Ressource nicht gefunden oder nicht im aktiven Workspace (`not_found`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Konflikt (`conflict`, bei Workspace-Bezug auch `no_workspace`) — z. B. ungültiger Zustandswechsel, festgeschriebener Beleg, Eindeutigkeitsverletzung oder kein aktiver Workspace.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ValidationError:
      description: Validierungsfehler (`validation_error`); `details.field` nennt das Feld.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Rate-Limit des API-Keys überschritten (`rate_limited`, Fixed-Window).
      headers:
        RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ServiceUnavailable:
      description: Dienst nicht konfiguriert (`billing_not_configured`, `github_not_configured`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    ActivityItem:
      description: Ein Eintrag der Projekt-Aktivitäten (`kind` unterscheidet Session, Commit, Timer).
      oneOf:
        - $ref: "#/components/schemas/ActivitySession"
        - $ref: "#/components/schemas/ActivityCommit"
        - $ref: "#/components/schemas/ActivityTimer"
      discriminator:
        propertyName: kind
        mapping:
          session: "#/components/schemas/ActivitySession"
          commit: "#/components/schemas/ActivityCommit"
          timer: "#/components/schemas/ActivityTimer"
    ActivitySession:
      type: object
      required:
        - kind
        - id
        - at
        - tool
        - started_at
        - running
        - input_tokens
        - output_tokens
        - tokens
      properties:
        kind:
          type: string
          const: session
        id:
          type: string
          format: uuid
        at:
          type: string
          format: date-time
        sortMs:
          type:
            - number
            - "null"
          description: Sortierschlüssel in ms (null ohne verwertbaren Zeitstempel).
        tool:
          type: string
        model:
          type:
            - string
            - "null"
        activity_context:
          type:
            - string
            - "null"
        folder_path:
          type:
            - string
            - "null"
        session_kind:
          type: string
          enum:
            - primary
            - subagent
        client_id:
          type:
            - string
            - "null"
        parent_client_id:
          type:
            - string
            - "null"
        collector_version:
          type:
            - string
            - "null"
          description: "Erfassende Kollektor-Version: `cli/<version>` für den Kollektor der CLI (X-TM-CLI-Version), sonst die Version des alten Kollektors (X-Collector-Version)."
        started_at:
          type: string
          format: date-time
        ended_at:
          type:
            - string
            - "null"
          format: date-time
        client_updated_at:
          type:
            - string
            - "null"
          format: date-time
        seconds:
          type:
            - integer
            - "null"
          description: Aktive Laufzeit in Sekunden.
        running:
          type: boolean
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        tokens:
          type: integer
        costMicros:
          type:
            - integer
            - "null"
          description: Kosten in Mikro-Euro (µ€); null ohne bekannten Modellpreis.
    ActivityCommit:
      type: object
      required:
        - kind
        - id
        - commit_sha
      properties:
        kind:
          type: string
          const: commit
        id:
          type: string
          format: uuid
        at:
          type:
            - string
            - "null"
          format: date-time
        sortMs:
          type:
            - number
            - "null"
        commit_sha:
          type: string
        message:
          type:
            - string
            - "null"
        author:
          type:
            - string
            - "null"
        branch:
          type:
            - string
            - "null"
        files_changed:
          type:
            - integer
            - "null"
        additions:
          type:
            - integer
            - "null"
        deletions:
          type:
            - integer
            - "null"
        author_login:
          type:
            - string
            - "null"
        author_avatar_url:
          type:
            - string
            - "null"
        source:
          type:
            - string
            - "null"
        repository_id:
          type:
            - string
            - "null"
          format: uuid
        repo_full_name:
          type:
            - string
            - "null"
        session_id:
          type:
            - string
            - "null"
          format: uuid
          description: KI-Session, aus der der Commit stammt.
    ActivityTimer:
      type: object
      required:
        - kind
        - id
        - at
        - seconds
      properties:
        kind:
          type: string
          const: timer
        id:
          type: string
          format: uuid
        at:
          type: string
          format: date-time
        sortMs:
          type:
            - number
            - "null"
        seconds:
          type: integer
        description:
          type:
            - string
            - "null"
    AiSession:
      type: object
      required:
        - id
        - workspace_id
        - tool
        - started_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        project_id:
          type:
            - string
            - "null"
          format: uuid
        project_assignment:
          type: string
          enum:
            - auto
            - manual
            - inherited
          description: Herkunft der Projektzuordnung. `auto` = per Ordnerregel (Ingest oder Nachziehen nach `learn`), folgt späteren Regeltreffern. `manual` = per `POST /ai-sessions/{id}/assign` zugeordnet; der Ingest überschreibt sie nicht mehr, nur eine erneute Zuordnung ändert sie. `inherited` = Subagent, der die Hand-Zuordnung seiner Hauptsession übernommen hat (rekursiv, auch für später eintreffende Subagenten); er folgt jeder weiteren Hand-Zuordnung der Hauptsession, bis er selbst von Hand zugeordnet wird. Wird das Projekt gelöscht, ist die Session wieder `auto`.
        project_assigned_at:
          type:
            - string
            - "null"
          format: date-time
          description: Zeitpunkt der letzten manuellen Zuordnung, bei `inherited` der Zeitpunkt der geerbten Hand-Zuordnung (null = automatisch oder unbekannt).
        tool:
          type: string
        started_at:
          type: string
          format: date-time
        ended_at:
          type:
            - string
            - "null"
          format: date-time
        folder_path:
          type:
            - string
            - "null"
        activity_context:
          type:
            - string
            - "null"
          description: Titel der Session bzw. Name des Subagenten (siehe IngestEvent.activity_context).
        work_paths:
          oneOf:
            - type: array
              items:
                $ref: "#/components/schemas/WorkPath"
            - type: "null"
          description: Ordner, in die die Session geschrieben hat, meistbeschriebene zuerst (höchstens 10).
        git_remote:
          type:
            - string
            - "null"
          description: Repository des Startordners als host/pfad.
        model:
          type:
            - string
            - "null"
        input_tokens:
          type:
            - integer
            - "null"
        output_tokens:
          type:
            - integer
            - "null"
        cache_read_input_tokens:
          type:
            - integer
            - "null"
        cache_creation_input_tokens:
          type:
            - integer
            - "null"
        cache_creation_5m_input_tokens:
          type:
            - integer
            - "null"
        cache_creation_1h_input_tokens:
          type:
            - integer
            - "null"
        long_context_usage:
          oneOf:
            - $ref: "#/components/schemas/LongContextUsage"
            - type: "null"
          description: |
            Token-Anteile der Anfragen über festen Kontext-Schwellen (siehe IngestEvent.long_context_usage); null = vom Kollektor nicht gemeldet.
        provider_cost:
          oneOf:
            - $ref: "#/components/schemas/ProviderCost"
            - type: "null"
          description: |
            Kosten laut Tool (siehe ProviderCost); null = nicht gemeldet. Bewertet nur ohne Tabellenpreis.
        active_seconds:
          type:
            - integer
            - "null"
          description: Kumulierte aktive Laufzeit in Sekunden (siehe IngestEvent.active_seconds).
        session_kind:
          type:
            - string
            - "null"
          enum:
            - primary
            - subagent
            - null
        parent_client_id:
          type:
            - string
            - "null"
          format: uuid
        client_id:
          type:
            - string
            - "null"
          format: uuid
        billability:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
          description: Override; null = von Projekt/Kunde erben.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ApiKey:
      type: object
      required:
        - id
        - name
        - key_prefix
        - permission
        - created_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          description: Wiedererkennbarer Präfix (die ersten 12 Zeichen, nicht geheim).
        permission:
          type: string
          enum:
            - read
            - write
          description: "`read`: Daten-API lesen, Kollektor-Ingest, Timer der lokalen Clients. `write`: zusätzlich Token-Tausch (POST /auth/token) mit allen Rechten des Nutzers. Nicht änderbar."
        last_used_at:
          type:
            - string
            - "null"
          format: date-time
        last_collector_version:
          type:
            - string
            - "null"
          description: "Zuletzt beim Ingest gemeldete Kollektor-Version dieses Keys: `cli/<version>` für den Kollektor der CLI (via X-TM-CLI-Version), sonst die Version des alten Kollektors (via X-Collector-Version); null, solange kein versionierter Kollektor gesendet hat."
        revoked_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
    ApiKeyCreated:
      type: object
      description: Antwort der Erstellung — Stammfelder plus einmaliger Klartext.
      required:
        - id
        - name
        - key_prefix
        - permission
        - created_at
        - key
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
        permission:
          type: string
          enum:
            - read
            - write
        created_at:
          type: string
          format: date-time
        key:
          type: string
          description: Klartext-Key `tmk_…` — wird NUR bei der Erstellung geliefert.
    AuthToken:
      type: object
      description: Ergebnis des Token-Tauschs (POST /auth/token).
      required:
        - access_token
        - token_type
        - expires_at
        - workspace_id
        - user_id
        - permission
      properties:
        access_token:
          type: string
          description: "Supabase-Access-Token (JWT) des Key-Inhabers — als `Authorization: Bearer <JWT>` senden."
        token_type:
          type: string
          const: bearer
        expires_at:
          type: string
          format: date-time
          description: Ablaufzeitpunkt (Standard 1 h nach Ausstellung).
        expires_in:
          type: integer
          minimum: 0
          description: Restlaufzeit in Sekunden zum Ausstellungszeitpunkt.
        workspace_id:
          type: string
          format: uuid
          description: Workspace des Keys (Vorschlag für `X-TM-Workspace`).
        user_id:
          type: string
          format: uuid
        permission:
          type: string
          enum:
            - read
            - write
          description: Berechtigung des Keys (beim Tausch immer `write`).
    BillingRuleFields:
      type: object
      description: Abrechnungsregeln (Billability-Standard und Rundung) für Kunden und Projekte. Jedes Feld optional (fehlt = nicht ändern); null bzw. "" = leeren (vom Kunden bzw. Workspace erben).
      properties:
        billable_default:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
        rounding_mode:
          type:
            - string
            - "null"
          enum:
            - none
            - ceil
            - nearest
            - null
        rounding_increment_minutes:
          type:
            - integer
            - "null"
          enum:
            - 1
            - 5
            - 10
            - 15
            - 30
            - 60
            - null
        minimum_entry_minutes:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 480
        include_ai_costs_default:
          type:
            - boolean
            - "null"
    BillingStatus:
      type: object
      required:
        - plan
        - entitlement
        - billing_configured
        - limits_enforced
        - subscription
        - usage
      properties:
        plan:
          type: string
          enum:
            - free
            - pro
          description: Bezahltes Abo (Pro bei Stripe-Status active, trialing oder past_due) — entscheidet über Checkout und Kundenportal. Den tatsächlichen Leistungsumfang trägt `entitlement`.
        entitlement:
          type: object
          required:
            - state
            - has_pro
            - trial_ends_at
            - trial_days_left
          description: Leistungsumfang je Nutzer (Abo, Gratis-Zugang oder Testphase).
          properties:
            state:
              type: string
              enum:
                - free
                - trial
                - pro
                - complimentary
            has_pro:
              type: boolean
              description: Pro-Umfang — unabhängig davon, ob die Tarifgrenzen gerade greifen.
            trial_ends_at:
              type:
                - string
                - "null"
              format: date-time
              description: Ende der Testphase (30 Tage ab Registrierung), auch wenn abgelaufen.
            trial_days_left:
              type:
                - integer
                - "null"
              description: Angebrochene Resttage — nur im Zustand `trial`.
        billing_configured:
          type: boolean
          description: Ob Stripe konfiguriert ist (sonst liefern Checkout/Portal 503).
        limits_enforced:
          type: boolean
          description: Ob die Tarifgrenzen derzeit greifen (Funktionsgates, 30-Tage-Fenster).
        subscription:
          type:
            - object
            - "null"
          description: Abo-Stand ohne Stripe-IDs; null = nie ein Abo.
          required:
            - status
            - current_period_end
            - cancel_at_period_end
            - has_customer
          properties:
            status:
              type:
                - string
                - "null"
            current_period_end:
              type:
                - string
                - "null"
              format: date-time
            cancel_at_period_end:
              type: boolean
            has_customer:
              type: boolean
              description: Stripe-Kunde vorhanden (Voraussetzung für das Kundenportal).
        usage:
          type: object
          required:
            - projects
            - ai_sessions
          description: Nutzung des Workspace. Mengen sind in keinem Tarif begrenzt.
          properties:
            projects:
              type: object
              required:
                - active
              properties:
                active:
                  type: integer
            ai_sessions:
              type: object
              required:
                - visible
                - hidden
                - hidden_cost_micros
              properties:
                visible:
                  type: integer
                  description: Sichtbare KI-Sessions (Kontroll-Sessions; ohne Pro die letzten 30 Tage).
                hidden:
                  type: integer
                  description: Ausgeblendete ältere Sessions (nur ohne Pro bei eingeschalteten Grenzen).
                hidden_cost_micros:
                  type: integer
                  description: KI-Kosten der ausgeblendeten Sessions in µ€.
    ContentWarning:
      type: object
      description: Hinweis zur Bausteinauswahl — blockiert nichts, erklärt aber eine sonst falsch wirkende Zahl.
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - missing_rate
            - unpriced_model
            - token_price_missing
            - runtime_rate_missing
        projectId:
          type: string
          format: uuid
          description: Bei `missing_rate`.
        projectName:
          type: string
          description: Bei `missing_rate`.
        model:
          type:
            - string
            - "null"
          description: Bei `unpriced_model`.
        sessions:
          type: integer
          description: Bei `unpriced_model`.
    CurrentWorkspace:
      type: object
      required:
        - id
        - name
        - role
        - user_id
        - auth
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        role:
          type: string
          enum:
            - owner
            - member
            - viewer
        user_id:
          type: string
          format: uuid
        auth:
          type: string
          enum:
            - session
            - api_key
          description: Zugangsweg der Anfrage (`session` umfasst Cookie und Bearer-JWT).
    Customer:
      type: object
      required:
        - id
        - workspace_id
        - name
        - created_at
        - updated_at
        - deleted_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        name:
          type: string
        customer_number:
          type:
            - string
            - "null"
        kind:
          type: string
          enum:
            - regular
            - internal
            - pro_bono
          default: regular
          description: "Kundenart: `regular` wird abgerechnet; `internal` (interner Kunde) und `pro_bono` (kostenlos) erwarten keinen Auftrag und zählen nicht als offene Leistung."
        billing_name:
          type:
            - string
            - "null"
        billing_address:
          type:
            - string
            - "null"
          description: Zusammengesetzte Anschrift (aus den Bausteinen, DIN 5008).
        billing_street:
          type:
            - string
            - "null"
        billing_address_extra:
          type:
            - string
            - "null"
        billing_postal_code:
          type:
            - string
            - "null"
        billing_city:
          type:
            - string
            - "null"
        country_code:
          type:
            - string
            - "null"
          description: ISO-3166-Alpha-2, großgeschrieben.
        vat_id:
          type:
            - string
            - "null"
        contact_name:
          type:
            - string
            - "null"
        contact_email:
          type:
            - string
            - "null"
        contact_phone:
          type:
            - string
            - "null"
        invoice_recipients:
          type: array
          items:
            $ref: "#/components/schemas/InvoiceRecipient"
        notes:
          type:
            - string
            - "null"
        default_hourly_rate_cents:
          type:
            - integer
            - "null"
          description: Standard-Stundensatz (Cent/h) — Vorlage für neue Projekte.
        ai_runtime_rate_cents:
          type:
            - integer
            - "null"
          description: Standardsatz der KI-Laufzeit (Cent/h) für den Rechnungsbaustein „KI-Nutzung“.
        buyer_reference:
          type:
            - string
            - "null"
          description: Käuferreferenz bzw. Leitweg-ID (EN 16931 BT-10) — wird in neue Rechnungen übernommen; bei XRechnung Pflicht.
        einvoice_format:
          type: string
          enum:
            - none
            - zugferd
            - xrechnung
          default: zugferd
          description: Bevorzugtes E-Rechnungsformat; wird in neue Rechnungen übernommen.
        currency:
          type: string
          enum:
            - EUR
            - USD
        payment_terms_days:
          type:
            - integer
            - "null"
        billable_default:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
        rounding_mode:
          type:
            - string
            - "null"
          enum:
            - none
            - ceil
            - nearest
            - null
        rounding_increment_minutes:
          type:
            - integer
            - "null"
        minimum_entry_minutes:
          type:
            - integer
            - "null"
        include_ai_costs_default:
          type:
            - boolean
            - "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - "null"
          format: date-time
          description: Gesetzt = archiviert.
    CustomerInput:
      description: Stammdaten und Abrechnungsregeln eines Kunden. Alle Felder optional (PATCH-Semantik); bei Textfeldern bedeutet "" = leeren (null).
      allOf:
        - $ref: "#/components/schemas/BillingRuleFields"
        - type: object
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 200
            customer_number:
              type:
                - string
                - "null"
              maxLength: 50
            kind:
              type: string
              enum:
                - regular
                - internal
                - pro_bono
              description: Kundenart (`regular` = Standard; `internal`, `pro_bono` = ohne Abrechnung).
            billing_name:
              type:
                - string
                - "null"
              maxLength: 200
            billing_address:
              type:
                - string
                - "null"
              maxLength: 1000
              description: Freitext-Anschrift älterer Clients; wird bei Übergabe von Bausteinen neu gesetzt.
            billing_street:
              type:
                - string
                - "null"
              maxLength: 200
            billing_address_extra:
              type:
                - string
                - "null"
              maxLength: 200
            billing_postal_code:
              type:
                - string
                - "null"
              maxLength: 20
            billing_city:
              type:
                - string
                - "null"
              maxLength: 200
            country_code:
              type:
                - string
                - "null"
              pattern: ^[A-Za-z]{2}$
            vat_id:
              type:
                - string
                - "null"
              maxLength: 50
            contact_name:
              type:
                - string
                - "null"
              maxLength: 200
            contact_email:
              type:
                - string
                - "null"
              format: email
              maxLength: 320
            contact_phone:
              type:
                - string
                - "null"
              maxLength: 50
            notes:
              type:
                - string
                - "null"
              maxLength: 5000
            invoice_recipients:
              type:
                - array
                - "null"
              maxItems: 20
              description: Rechnungsempfänger. Leere Zeilen werden verworfen, doppelte Adressen zusammengelegt, höchstens 20; null = keine.
              items:
                type: object
                properties:
                  email:
                    type:
                      - string
                      - "null"
                  description:
                    type:
                      - string
                      - "null"
            default_hourly_rate_cents:
              type:
                - integer
                - "null"
              minimum: 0
              maximum: 1000000
            ai_runtime_rate_cents:
              type:
                - integer
                - "null"
              minimum: 0
              maximum: 1000000
            buyer_reference:
              type:
                - string
                - "null"
              maxLength: 200
              description: Käuferreferenz bzw. Leitweg-ID (Standardwert neuer Rechnungen).
            einvoice_format:
              type: string
              enum:
                - none
                - zugferd
                - xrechnung
              description: Bevorzugtes E-Rechnungsformat (`none` = nur PDF).
            currency:
              type: string
              enum:
                - EUR
                - USD
            payment_terms_days:
              type:
                - integer
                - "null"
              minimum: 0
              maximum: 365
    DashboardSummary:
      type: object
      description: Ergebnis von GET /dashboard/summary. Geld in µ€, Zeiten in Sekunden, Token als Netto-Token.
      required:
        - totals
        - perProject
        - perCustomer
        - daily
        - billableRevenueByMonth
        - perModel
        - perTool
        - byKind
      properties:
        totals:
          $ref: "#/components/schemas/UsageFigures"
        perProject:
          type: array
          description: Je Projekt; `id` `__unassigned__` = „Ohne Projekt“.
          items:
            $ref: "#/components/schemas/UsageSummary"
        perCustomer:
          type: array
          description: Je Kunde; `id` `__no_customer__` = „Ohne Kunde“.
          items:
            $ref: "#/components/schemas/UsageSummary"
        daily:
          type: array
          items:
            type: object
            required:
              - date
              - human_seconds
              - ai_seconds
              - tokens
              - input_tokens
              - output_tokens
              - cache_read_input_tokens
              - cache_creation_input_tokens
              - cost_micros
            properties:
              date:
                type: string
                format: date
                description: UTC-Kalendertag.
              human_seconds:
                type: integer
              ai_seconds:
                type: integer
              tokens:
                type: integer
              input_tokens:
                type: integer
              output_tokens:
                type: integer
              cache_read_input_tokens:
                type: integer
              cache_creation_input_tokens:
                type: integer
              cost_micros:
                type: integer
        billableRevenueByMonth:
          type: array
          description: Leistungswert je Kalendermonat (Berliner Tage).
          items:
            type: object
            required:
              - period_month
              - billable_revenue_micros
            properties:
              period_month:
                type: string
                format: date
              billable_revenue_micros:
                type: integer
        perModel:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/TokenBreakdown"
              - type: object
                required:
                  - model
                  - cost_micros
                  - unpriced
                properties:
                  model:
                    type: string
                  cost_micros:
                    type: integer
                  unpriced:
                    type: boolean
                    description: Token vorhanden, aber kein Preis hinterlegt (Kosten als 0 gewertet).
        perTool:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/TokenBreakdown"
              - type: object
                required:
                  - tool
                properties:
                  tool:
                    type: string
        byKind:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/TokenBreakdown"
              - type: object
                required:
                  - kind
                properties:
                  kind:
                    type: string
                    enum:
                      - primary
                      - subagent
    UsageFigures:
      type: object
      description: Kennzahlen eines Zeitraums (gesamt bzw. je Projekt/Kunde ohne id/name).
      required:
        - human_seconds
        - ai_seconds
        - input_tokens
        - output_tokens
        - cache_read_input_tokens
        - cache_creation_input_tokens
        - tokens
        - revenue_micros
        - cost_micros
        - margin_micros
        - unpriced_sessions
        - billable_seconds
        - non_billable_seconds
        - billable_revenue_micros
        - billable_cost_micros
      properties:
        human_seconds:
          type: integer
          description: Erfasste Arbeitszeit (Timer).
        ai_seconds:
          type: integer
          description: KI-Laufzeit.
        input_tokens:
          type: integer
          description: Neuer Input = frischer Input + Cache-Erzeugung (ohne Cache-Reads).
        output_tokens:
          type: integer
        cache_read_input_tokens:
          type: integer
          description: Gecacht gelesene Input-Token (nicht Teil von `tokens`).
        cache_creation_input_tokens:
          type: integer
        cache_creation_reporting:
          type: string
          enum:
            - complete
            - partial
            - unreported
          description: Wie vollständig die Tools die Cache-Erzeugung melden.
        tokens:
          type: integer
          description: Netto-Token = neuer Input + Output.
        revenue_micros:
          type: integer
          description: Leistungswert aus Stundensatz × erfasster Zeit.
        cost_micros:
          type: integer
          description: |
            KI-Kosten aus den Modellpreisen (USD-Listenpreise mit dem EZB-Kurs vom Tag des Session-Starts in Euro umgerechnet). Untergrenze, wenn `unpriced_sessions` > 0.
        margin_micros:
          type: integer
          description: Leistungswert − KI-Kosten. Obergrenze, wenn `unpriced_sessions` > 0.
        unpriced_sessions:
          type: integer
          description: |
            Sessions mit Token, aber ohne greifenden Modellpreis — mit 0 € in `cost_micros`. Größer 0 heißt: die Kosten sind eine Untergrenze (Anzeige „mind.“).
        billable_seconds:
          type: integer
          description: Gerundete abrechenbare Timer-Dauer.
        non_billable_seconds:
          type: integer
        billable_revenue_micros:
          type: integer
        billable_cost_micros:
          type: integer
    UsageSummary:
      allOf:
        - type: object
          required:
            - id
            - name
          properties:
            id:
              type: string
            name:
              type: string
        - $ref: "#/components/schemas/UsageFigures"
    TokenBreakdown:
      type: object
      required:
        - input_tokens
        - output_tokens
        - cache_read_input_tokens
        - cache_creation_input_tokens
        - tokens
        - sessions
      properties:
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        cache_read_input_tokens:
          type: integer
        cache_creation_input_tokens:
          type: integer
        cache_creation_reporting:
          type: string
          enum:
            - complete
            - partial
            - unreported
        tokens:
          type: integer
        sessions:
          type: integer
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Maschinenlesbarer Fehlercode.
            message:
              type: string
            details:
              type: object
              additionalProperties: true
    EInvoiceRequirement:
      type: object
      description: Fehlende oder ungültige Pflichtangabe der E-Rechnung, vor der XML-Erzeugung geprüft.
      required:
        - field
        - message
        - severity
        - fix
      properties:
        field:
          type: string
          description: Feld der Rechnung (Spaltenname) bzw. `items.<id>.<feld>` für eine Position.
          examples:
            - recipient_postal_code
        message:
          type: string
          examples:
            - Postleitzahl des Empfängers fehlt.
        severity:
          type: string
          enum:
            - error
            - warning
          description: "`error` verhindert das Stellen als E-Rechnung, `warning` nicht."
        fix:
          type: string
          enum:
            - template
            - customer
            - invoice
            - content
          description: Wo es sich beheben lässt (Dokumentvorlage, Kunde, Rechnung, Positionen).
    EInvoiceFinding:
      type: object
      description: Befund der XML-Prüfung (XSD bzw. Schematron EN 16931/XRechnung) im Wortlaut der Regel.
      required:
        - id
        - severity
        - message
      properties:
        id:
          type: string
          description: Regelkennung, z. B. `BR-DE-15`, `xsd` oder `input`.
        severity:
          type: string
          enum:
            - error
            - warning
        message:
          type: string
        location:
          type: string
          description: Fundstelle (XPath bzw. Pfad im Eingabemodell).
    EInvoiceRejectionDetails:
      type: object
      description: |
        `error.details` der 422-Antwort, wenn eine Rechnung nicht als E-Rechnung gestellt werden kann. `findings` ist leer, wenn schon Pflichtangaben fehlten (dann wird kein XML erzeugt).
      required:
        - field
        - format
        - requirements
        - findings
      properties:
        field:
          type: string
          enum:
            - einvoice
        format:
          type: string
          enum:
            - zugferd
            - xrechnung
        requirements:
          type: array
          items:
            $ref: "#/components/schemas/EInvoiceRequirement"
        findings:
          type: array
          items:
            $ref: "#/components/schemas/EInvoiceFinding"
    EInvoiceReadiness:
      type: object
      description: |
        Vorprüfung eines Entwurfs: was beim Stellen als E-Rechnung fehlen würde. `format: null` = der Entwurf wird ohne E-Rechnung gestellt (dann `ready: true` und leere Listen).
      required:
        - format
        - ready
        - validated
        - requirements
        - findings
      properties:
        format:
          type:
            - string
            - "null"
          enum:
            - zugferd
            - xrechnung
            - null
        ready:
          type: boolean
          description: |
            `true` = keine Fehler. Ohne `validate=true` beruht das nur auf den Pflichtangaben; die XML-Prüfung beim Stellen kann dann noch Befunde liefern.
        validated:
          type: boolean
          description: "`true` = das XML wurde erzeugt und gegen XSD und Schematron geprüft."
        requirements:
          type: array
          items:
            $ref: "#/components/schemas/EInvoiceRequirement"
        findings:
          type: array
          description: Befunde der XML-Prüfung (nur mit `validate=true` und vollständigen Pflichtangaben).
          items:
            $ref: "#/components/schemas/EInvoiceFinding"
    Folder:
      type: object
      required:
        - id
        - workspace_id
        - project_id
        - folder_path
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        folder_path:
          type: string
          description: Absoluter Ordnerpfad oder Repository (`host/org/repo`; URLs und `git@host:org/repo` werden beim Anlegen normalisiert). Neue oder geänderte Regeln entscheiden die automatisch zugeordneten KI-Sessions sofort neu.
        fallback:
          type: boolean
          description: "Auffangregel: ordnet nur zu, wenn weder eine gewöhnliche Ordner-/Repository-Regel noch geschriebene Ordner, Stichworte oder die Eltern-Session ein Projekt ergeben — für Sammelordner, in denen für viele Projekte gearbeitet wird."
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    GitCommit:
      type: object
      required:
        - id
        - workspace_id
        - commit_sha
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        project_id:
          type:
            - string
            - "null"
          format: uuid
        commit_sha:
          type: string
        message:
          type:
            - string
            - "null"
        author:
          type:
            - string
            - "null"
        branch:
          type:
            - string
            - "null"
        folder_path:
          type:
            - string
            - "null"
        committed_at:
          type:
            - string
            - "null"
          format: date-time
        tool:
          type:
            - string
            - "null"
        client_id:
          type:
            - string
            - "null"
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    GithubInstallation:
      type: object
      required:
        - id
        - account_login
        - account_type
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
        account_login:
          type: string
          description: GitHub-Login des verbundenen Accounts/der Org.
        account_type:
          type: string
          enum:
            - User
            - Organization
        account_avatar_url:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - active
            - suspended
            - revoked
          description: Lebenszyklus der Installation (revoked bleibt für die Reconnect-UI sichtbar).
        created_at:
          type: string
          format: date-time
    GithubInstallationRepo:
      type: object
      required:
        - github_repo_id
        - owner
        - name
        - full_name
        - private
        - default_branch
      properties:
        github_repo_id:
          type: integer
          format: int64
        owner:
          type: string
        name:
          type: string
        full_name:
          type: string
        private:
          type: boolean
        default_branch:
          type: string
        linked_project_id:
          type:
            - string
            - "null"
          format: uuid
          description: Gesetzt, wenn dieses Repo im Workspace bereits verknüpft ist.
        linked_project_name:
          type:
            - string
            - "null"
    GithubRepository:
      type: object
      required:
        - id
        - installation_id
        - github_repo_id
        - owner
        - name
        - full_name
        - private
        - default_branch
        - sync_status
      properties:
        id:
          type: string
          format: uuid
        installation_id:
          type: string
          format: uuid
        project_id:
          type:
            - string
            - "null"
          format: uuid
        github_repo_id:
          type: integer
          format: int64
          description: GitHub `databaseId` (stabil bei Rename/Transfer).
        owner:
          type: string
        name:
          type: string
        full_name:
          type: string
        private:
          type: boolean
        default_branch:
          type: string
        last_synced_at:
          type:
            - string
            - "null"
          format: date-time
        sync_status:
          type: string
          enum:
            - idle
            - syncing
            - error
        sync_error:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time
    LongContextBucket:
      type: object
      description: |
        Token-Anteile der Anfragen über einer Kontext-Schwelle — gleiche Bedeutung wie die gleichnamigen Felder des Ingest-Events (disjunkt: frisch / Cache-Lesen / Cache-Erzeugung).
      properties:
        input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
        output_tokens:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
        cache_read_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
        cache_creation_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
        cache_creation_5m_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
        cache_creation_1h_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
    LongContextUsage:
      type: object
      description: |
        Eimer je Kontext-Schwelle in Tokens. Eine Anfrage zählt in jeden Eimer, dessen Schwelle ihr Kontext überschreitet (genau 200.000 zählt nicht zu „200000“). Bekannte Schwellen: 100000 (Claude Haiku 5.5), 200000 (Claude-Long-Context), 272000 (OpenAI). Unbekannte Schlüssel ignoriert der Server.
      properties:
        "100000":
          $ref: "#/components/schemas/LongContextBucket"
        "200000":
          $ref: "#/components/schemas/LongContextBucket"
        "272000":
          $ref: "#/components/schemas/LongContextBucket"
    ProviderCost:
      type: object
      required:
        - currency
      description: |
        Vom erfassenden Tool selbst gemeldete, KUMULIERTE Kosten der Session (bisher nur Cursor: Summe von `tokenUsage.totalCents` bzw. `chargedCents` der zugeordneten Nutzungsereignisse). Beträge in Mikro-Einheiten der Währung (1 USD = 1.000.000, 1 Cent = 10.000) — ganzzahlig und damit verlustfrei für Cent-Bruchteile. Bewertet wird nur der Listenpreis und nur, wenn die Preistabelle für die Session KEINEN Preis kennt (z. B. Cursor-eigene Modelle wie composer-1); Fremdwährung mit dem EZB-Kurs vom Berliner Tag des Session-Starts.
      properties:
        currency:
          type: string
          pattern: ^[A-Z]{3}$
          description: ISO-4217-Währungscode (Cursor meldet USD).
        list_micros:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
          description: Listenpreis-Schätzung des Tools (Cursor `totalCents`). Grundlage der Bewertung.
        charged_micros:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000000000
          description: |
            Tatsächlich abgerechneter Betrag (Cursor `chargedCents`, im Abo-Kontingent oft 0). Nur zur Information, fließt nicht in die Kosten.
    WorkPath:
      type: object
      required:
        - path
        - writes
      properties:
        path:
          type: string
          minLength: 1
          maxLength: 1024
          description: Absoluter Ordnerpfad.
        writes:
          type: integer
          minimum: 1
          maximum: 1000000000
          description: Anzahl der Schreibzugriffe (Dateien angelegt oder geändert) in diesem Ordner.
    LimitWindow:
      type: object
      required:
        - kind
        - used_percent
      properties:
        kind:
          type: string
          enum:
            - session
            - weekly
            - model
          description: "`session` = 5-Stunden-Fenster, `weekly` = Woche (alle Modelle), `model` = modellbezogenes Wochenfenster."
        label:
          type:
            - string
            - "null"
          maxLength: 60
          description: Modell- oder Limitname bei `kind = model` (z. B. Sonnet).
        used_percent:
          type: number
          minimum: 0
          maximum: 1000
          description: Genutzter Anteil in Prozent (kann 100 übersteigen).
        resets_at:
          type:
            - string
            - "null"
          format: date-time
        window_minutes:
          type:
            - integer
            - "null"
          description: Fensterlänge in Minuten (300 = 5 h, 10080 = Woche), sofern bekannt.
    LimitExtraUsage:
      type: object
      description: Claude Extra Usage (monatliches Zusatzbudget) in Währungseinheiten.
      properties:
        enabled:
          type: boolean
        used:
          type:
            - number
            - "null"
        limit:
          type:
            - number
            - "null"
        currency:
          type:
            - string
            - "null"
          pattern: ^[A-Z]{3}$
        used_percent:
          type:
            - number
            - "null"
    LimitCredits:
      type: object
      description: Codex-Credits.
      properties:
        has_credits:
          type: boolean
        unlimited:
          type: boolean
        balance:
          type:
            - number
            - "null"
    LimitSnapshot:
      type: object
      required:
        - provider
        - account_key
        - account_label
        - plan
        - status
        - windows
        - extra_usage
        - credits
        - captured_at
        - checked_at
        - stale
      properties:
        provider:
          type: string
          enum:
            - claude
            - codex
        account_key:
          type: string
        account_label:
          type:
            - string
            - "null"
        plan:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - ok
            - auth_required
            - error
          description: Ergebnis des letzten Abrufversuchs.
        windows:
          type: array
          items:
            $ref: "#/components/schemas/LimitWindow"
        extra_usage:
          oneOf:
            - $ref: "#/components/schemas/LimitExtraUsage"
            - type: "null"
        credits:
          oneOf:
            - $ref: "#/components/schemas/LimitCredits"
            - type: "null"
        captured_at:
          type:
            - string
            - "null"
          format: date-time
          description: Letzter erfolgreicher Abruf (Stand der Fenster); null = noch nie.
        checked_at:
          type: string
          format: date-time
          description: Letzter Abrufversuch.
        stale:
          type: boolean
          description: Kein erfolgreicher Abruf, älter als 15 Minuten oder letzter Abruf fehlgeschlagen.
    Invoice:
      type: object
      description: Rechnungskopf (Spalten von `invoices`). Geld in µ€, Prozentsätze in Basispunkten.
      required:
        - id
        - workspace_id
        - customer_id
        - invoice_number
        - status
        - title
        - issue_date
        - currency
        - tax_mode
        - tax_rate_bp
        - layout
        - created_at
        - updated_at
        - deleted_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        customer_id:
          type:
            - string
            - "null"
          format: uuid
        order_id:
          type:
            - string
            - "null"
          format: uuid
        is_advance:
          type: boolean
          description: Anzahlungsrechnung zum Auftrag. Eine spätere Rechnung zum selben Auftrag zieht sie über den Baustein `advance` ab. Stornos erben das Kennzeichen.
        invoice_number:
          type: string
          description: Endgültige Nummer oder Platzhalter `ENTWURF-…` (die echte vergibt das Festschreiben).
        status:
          type: string
          enum:
            - draft
            - final
            - sent
            - paid
            - cancelled
        title:
          type: string
        issue_date:
          type: string
          format: date
        service_from:
          type:
            - string
            - "null"
          format: date
        service_to:
          type:
            - string
            - "null"
          format: date
        due_date:
          type:
            - string
            - "null"
          format: date
        payment_terms_days:
          type:
            - integer
            - "null"
        recipient_name:
          type:
            - string
            - "null"
        recipient_address:
          type:
            - string
            - "null"
        recipient_vat_id:
          type:
            - string
            - "null"
        recipient_email:
          type:
            - string
            - "null"
        sender_name:
          type:
            - string
            - "null"
        sender_address:
          type:
            - string
            - "null"
        sender_vat_id:
          type:
            - string
            - "null"
        sender_tax_number:
          type:
            - string
            - "null"
        sender_email:
          type:
            - string
            - "null"
        sender_phone:
          type:
            - string
            - "null"
        sender_bank_details:
          type:
            - string
            - "null"
        sender_website:
          type:
            - string
            - "null"
        sender_register:
          type:
            - string
            - "null"
        sender_register_court:
          type:
            - string
            - "null"
        sender_managing_directors:
          type:
            - string
            - "null"
        signer_name:
          type:
            - string
            - "null"
        greeting_text:
          type:
            - string
            - "null"
        recipient_street:
          type:
            - string
            - "null"
          description: Empfängeranschrift in Einzelfeldern. Beim Anlegen aus dem Kunden übernommen; `recipient_address` ist daraus zusammengesetzt. Rechnungen von vor 0112 tragen nur die Textanschrift.
        recipient_address_extra:
          type:
            - string
            - "null"
        recipient_postal_code:
          type:
            - string
            - "null"
        recipient_city:
          type:
            - string
            - "null"
        recipient_country_code:
          type:
            - string
            - "null"
          description: ISO-3166-Alpha-2, großgeschrieben.
        recipient_customer_number:
          type:
            - string
            - "null"
          readOnly: true
          description: Kundennummer des Empfängers (BT-46 der E-Rechnung). Im Entwurf aus dem Kunden, beim Festschreiben eingefroren (nur lesbar); PDF und XML zeigen diese Nummer.
        sender_street:
          type:
            - string
            - "null"
          description: Absenderanschrift in Einzelfeldern. Im Entwurf live aus der Rechnungsvorlage, beim Festschreiben eingefroren.
        sender_address_extra:
          type:
            - string
            - "null"
        sender_postal_code:
          type:
            - string
            - "null"
        sender_city:
          type:
            - string
            - "null"
        sender_country_code:
          type:
            - string
            - "null"
          description: ISO-3166-Alpha-2, großgeschrieben.
        sender_iban:
          type:
            - string
            - "null"
          description: IBAN kompakt (ohne Leerzeichen). Im Entwurf live aus der Rechnungsvorlage.
        sender_bic:
          type:
            - string
            - "null"
        sender_account_holder:
          type:
            - string
            - "null"
        buyer_reference:
          type:
            - string
            - "null"
          description: Käuferreferenz bzw. Leitweg-ID (BT-10), beim Anlegen aus dem Kunden.
        order_reference:
          type:
            - string
            - "null"
          description: Bestellnummer des Kunden (BT-13).
        tax_exemption_reason:
          type:
            - string
            - "null"
          description: Grund der Steuerbefreiung (BT-120), Pflicht bei `exempt`/`reverse_charge`.
        tax_exemption_code:
          type:
            - string
            - "null"
          description: VATEX-Code der Steuerbefreiung (BT-121), z. B. `VATEX-EU-AE`.
        einvoice_format:
          type: string
          enum:
            - none
            - zugferd
            - xrechnung
          description: E-Rechnungsformat. Neue Rechnungen übernehmen es aus dem Kunden; Rechnungen von vor der E-Rechnung tragen `none`.
        einvoice_validated_at:
          type:
            - string
            - "null"
          format: date-time
          description: Zeitpunkt der letzten E-Rechnungs-Prüfung (nur lesbar).
        einvoice_validation:
          type:
            - object
            - "null"
          additionalProperties: true
          description: Prüfbericht der letzten E-Rechnungs-Prüfung (nur lesbar).
        pdf_path:
          type:
            - string
            - "null"
          description: Speicherpfad des beim Festschreiben abgelegten PDFs (null = keins).
        intro_text:
          type:
            - string
            - "null"
        closing_text:
          type:
            - string
            - "null"
        payment_note:
          type:
            - string
            - "null"
        currency:
          type: string
          enum:
            - EUR
            - USD
            - CHF
        tax_mode:
          type: string
          enum:
            - standard
            - small_business
            - reverse_charge
            - exempt
        tax_rate_bp:
          type: integer
        tax_note:
          type:
            - string
            - "null"
        discount_bp:
          type:
            - integer
            - "null"
        discount_label:
          type:
            - string
            - "null"
        layout:
          $ref: "#/components/schemas/InvoiceLayoutConfig"
        content_config:
          $ref: "#/components/schemas/InvoiceContentConfig"
        source_project_ids:
          type:
            - array
            - "null"
          items:
            type: string
            format: uuid
        wizard_step:
          type:
            - string
            - "null"
          enum:
            - rahmen
            - inhalte
            - nachweise
            - pruefen
            - null
          description: Zuletzt geöffneter Schritt des Rechnungs-Assistenten.
        show_project_column:
          type:
            - boolean
            - "null"
          description: Projekt je Position auf der Rechnung zeigen — nur für diese Rechnung. null = Einstellung der Rechnungsvorlage (`layout.show_project_column`).
        show_customer_number:
          type:
            - boolean
            - "null"
          description: Kundennummer in den Kopfdaten zeigen — nur für diese Rechnung. null = Einstellung der Rechnungsvorlage (`layout.show_customer_number`).
        show_due_date:
          type:
            - boolean
            - "null"
          description: Fälligkeit („Fällig am“) in den Kopfdaten zeigen — nur für diese Rechnung. null = Einstellung der Rechnungsvorlage (`layout.show_due_date`).
        show_service_period:
          type:
            - boolean
            - "null"
          description: Leistungszeitraum in den Kopfdaten zeigen — nur für diese Rechnung. null = Einstellung der Rechnungsvorlage (`layout.show_service_period`).
        finalized_at:
          type:
            - string
            - "null"
          format: date-time
        cancels_invoice_id:
          type:
            - string
            - "null"
          format: uuid
          description: Nur auf einem Stornobeleg — die Rechnung, die er aufhebt.
        cancelled_at:
          type:
            - string
            - "null"
          format: date-time
          description: Nur auf dem stornierten Original.
        cancel_reason:
          type:
            - string
            - "null"
          description: Nur auf dem stornierten Original.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - "null"
          format: date-time
          description: Gesetzt = archiviert.
    InvoiceCandidates:
      type: object
      description: Auswahlgrundlage.
      properties:
        period:
          type: object
          properties:
            from:
              type: string
            to:
              type: string
        customer:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            defaultHourlyRateCents:
              type:
                - integer
                - "null"
            aiRuntimeRateCents:
              type:
                - integer
                - "null"
            paymentTermsDays:
              type:
                - integer
                - "null"
        projects:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              hourlyRateCents:
                type:
                  - integer
                  - "null"
              effectiveRateCents:
                type:
                  - integer
                  - "null"
              selected:
                type: boolean
        timers:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              day:
                type: string
                format: date
              projectId:
                type:
                  - string
                  - "null"
              projectName:
                type: string
              description:
                type:
                  - string
                  - "null"
              billableSeconds:
                type: integer
              activeSeconds:
                type: integer
              billable:
                type: boolean
              billedOn:
                type:
                  - string
                  - "null"
              startedAt:
                type:
                  - string
                  - "null"
                format: date-time
              endedAt:
                type:
                  - string
                  - "null"
                format: date-time
              inFinalStatements:
                type: array
                items:
                  type: string
                description: Nummern festgeschriebener Leistungsnachweise, die den Eintrag belegen — wählbar, aber nicht mehr änderbar oder löschbar.
              split:
                type: boolean
                description: Aus einer Teilung hervorgegangen — abgespalten oder Ursprung eines abgespaltenen Rests.
              orderSeconds:
                type:
                  - integer
                  - "null"
                description: Nur bei Rechnung zum Auftrag — Sekunden dieses Eintrags im Auftrag (0 = keine).
              elsewhere:
                type: array
                items:
                  type: string
                description: Wo der Eintrag bzw. sein Rest sonst liegt (Auftragstitel oder „nicht beauftragt“).
        sessions:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              day:
                type: string
                format: date
              projectId:
                type:
                  - string
                  - "null"
              projectName:
                type: string
              model:
                type:
                  - string
                  - "null"
              tool:
                type:
                  - string
                  - "null"
              netTokens:
                type: integer
              costMicros:
                type:
                  - integer
                  - "null"
              activeSeconds:
                type: integer
              billable:
                type: boolean
              billedOn:
                type:
                  - string
                  - "null"
        expenses:
          type: array
          description: |
            Einzelkosten der gewählten Projekte bis zum Ende des Leistungszeitraums (auch ältere); billedOn = Rechnungsnummer, auf der sie schon weiterbelastet sind.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              projectId:
                type: string
                format: uuid
              projectName:
                type: string
              date:
                type: string
                format: date
              title:
                type: string
              description:
                type:
                  - string
                  - "null"
              supplier:
                type:
                  - string
                  - "null"
              netCents:
                type: integer
              vatPaid:
                type: boolean
              vatCents:
                type: integer
              billedOn:
                type:
                  - string
                  - "null"
        lastBilledTo:
          type:
            - string
            - "null"
          format: date
        order:
          type:
            - object
            - "null"
          description: Auftrag der Rechnung mit Stand aus der Auftragsbindung (null = Rechnung ohne Auftrag). Dann trägt jeder Zeiteintrag `orderSeconds` (Anteil in diesem Auftrag) und `elsewhere`.
          properties:
            id:
              type: string
              format: uuid
            title:
              type: string
            allocatedSeconds:
              type: integer
            openSeconds:
              type: integer
            timeBudgetSeconds:
              type:
                - integer
                - "null"
        advances:
          type: array
          description: Gestellte Anzahlungsrechnungen desselben Auftrags, die in dieser Rechnung abgezogen werden können (Baustein „Anzahlung abziehen“). Leer ohne Auftrag und an einer Anzahlungsrechnung selbst.
          items:
            type: object
            properties:
              invoiceId:
                type: string
                format: uuid
              invoiceNumber:
                type: string
              issueDate:
                type: string
                format: date
              taxableNetMicros:
                type: integer
              exemptNetMicros:
                type: integer
              taxMicros:
                type: integer
              grossMicros:
                type: integer
              taxMode:
                type: string
              taxRateBp:
                type: integer
              paidMicros:
                type: integer
        fixedPrice:
          type:
            - object
            - "null"
          description: Festpreis-Auftrag der Rechnung (Baustein „Festpreis“); null ohne Auftrag, bei Time & Material und an einer Anzahlungsrechnung. `billedMicros` = Festpreis-Positionen gestellter Rechnungen desselben Auftrags außer dieser (Stornos saldieren sich heraus).
          properties:
            orderId:
              type: string
              format: uuid
            orderTitle:
              type: string
            priceMicros:
              type: integer
            billedMicros:
              type: integer
            projects:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
    InvoiceContentConfig:
      type: object
      description: |
        Bausteine einer Rechnung. Gespeichert werden nur Abweichungen vom Standard: abgewählte abrechenbare Einträge, ausdrücklich gewählte nicht abrechenbare, abgewählte KI-Zeilen.
      properties:
        blocks:
          type: array
          items:
            type: string
            enum:
              - time
              - ai
              - expenses
              - fixed_price
              - manual
              - discount
              - advance
              - text
        time:
          type: object
          properties:
            grouping:
              type: string
              enum:
                - project
                - day
                - entry
                - rate
            excludedTimerIds:
              type: array
              items:
                type: string
            includedTimerIds:
              type: array
              items:
                type: string
            keepWholeTimerIds:
              type: array
              items:
                type: string
              description: "Rechnung zum Auftrag: Einträge, die nur teilweise im Auftrag liegen, werden beim Speichern geteilt — außer sie stehen hier (dann ganz auf der Rechnung)."
            rateOverridesCents:
              type: object
              additionalProperties:
                type: integer
                minimum: 0
              description: Stundensatz nur für diese Rechnung je Projekt-ID (Cent/h).
            labels:
              type: object
              description: Bezeichnung der Arbeitszeit-Positionen je Gruppierung. Bei `rate` der Titel der Position (leer = „Arbeitszeit"), sonst ein Präfix vor Projekt, Tag bzw. Eintrag (leer = kein Präfix). Höchstens 120 Zeichen.
              properties:
                project:
                  type: string
                  default: Arbeitszeit
                day:
                  type: string
                  default: ""
                entry:
                  type: string
                  default: ""
                rate:
                  type: string
                  default: Arbeitszeit
        ai:
          type: object
          properties:
            mode:
              type: string
              enum:
                - cost
                - tokens
                - runtime
            grouping:
              type: string
              enum:
                - project
                - model
                - single
            markupBp:
              type: integer
              minimum: 0
              maximum: 100000
            tokenPriceMicros:
              type: integer
              minimum: 0
            runtimeRateCents:
              type:
                - integer
                - "null"
              minimum: 0
              description: null = Satz des Kunden (`customers.ai_runtime_rate_cents`).
            excludedKeys:
              type: array
              items:
                type: string
              description: Abgewählte Nutzungszeilen im Format `<projectId|none>|<model|unknown>`.
        expenses:
          type: object
          properties:
            includedExpenseIds:
              type: array
              items:
                type: string
              description: |
                Ausdrücklich gewählte Einzelkosten (`project_expenses.id`) — je eine Position `expense` zum Nettobetrag.
        discounts:
          type: array
          maxItems: 20
          description: |
            Rabatte (Baustein `discount`, wirksam nur, solange er in `blocks` steht). Jeder Rabatt wird eine negative Position `discount` hinter allen anderen (bei gemischtem Bezug je Steueranteil eine). Bezugsgröße ist die Summe der Positionen im Bezug; Rabatte beziehen sich nie auf andere Rabatte. Ein fester Betrag wird nach den Beträgen der Bezugspositionen auf Projekte und Steueranteile verteilt. Der Server bildet die Rabattpositionen bei jeder Änderung der Inhalte neu — auch beim Anlegen, Ändern und Löschen eigener Positionen.
          items:
            type: object
            required:
              - id
            properties:
              id:
                type: string
                maxLength: 100
                description: Stabiler Schlüssel des Rabatts.
              label:
                type: string
                maxLength: 120
                description: Text auf der Rechnung; leer = „Rabatt“.
              type:
                type: string
                enum:
                  - percent
                  - amount
                default: percent
              percentBp:
                type: integer
                minimum: 0
                maximum: 10000
                description: Nur `percent`, in Basispunkten (1000 = 10 %).
              amountCents:
                type: integer
                minimum: 0
                maximum: 100000000
                description: Nur `amount`, abzuziehender Betrag in Cent.
              scope:
                type: string
                enum:
                  - invoice
                  - blocks
                  - items
                default: invoice
                description: Ganze Rechnung, gewählte Bausteine oder gewählte Positionen.
              blocks:
                type: array
                items:
                  type: string
                  enum:
                    - time
                    - ai
                    - expenses
                    - manual
                description: Nur `scope = blocks`.
              itemRefs:
                type: array
                items:
                  type: string
                description: |
                  Nur `scope = items`. Schlüssel der Positionen: `item:<invoice_items.id>` für eigene Positionen, `<kind>:<source.key>` für erzeugte (der Gruppenschlüssel bleibt beim Neuerzeugen gleich, die ID der Position nicht).
        advances:
          type: object
          description: |
            Baustein „Anzahlung abziehen“ (`advance`, wirksam nur, solange er in `blocks` steht): gestellte Anzahlungsrechnungen desselben Auftrags, die noch nirgends abgezogen sind, werden abgezogen; die Rechnung ist dann eine Endrechnung (§ 14 Abs. 5 Satz 2 UStG): Steuer über die gesamte Leistung, je Anzahlung eine Position `advance` hinter allen Positionen und Rabatten mit −Netto (`amount_micros`) und −ausgewiesener Steuer (`tax_micros`). Die Summen zeigen den Abzug in `prepaid*`, den Rest in `amountDueMicros`. Gespeichert wird nur, was NICHT abgezogen werden soll.
          properties:
            excludedInvoiceIds:
              type: array
              items:
                type: string
                format: uuid
              description: Anzahlungsrechnungen, die in dieser Rechnung nicht abgezogen werden.
        fixedPrice:
          type: object
          description: |
            Baustein „Festpreis“ (`fixed_price`, wirksam nur, solange er in `blocks` steht und die Rechnung zu einem Festpreis-Auftrag gehört): eine Position `fixed_price` mit dem noch offenen Rest (`rest`, Standard), einem Prozentsatz (`percent`) oder einem festen Nettobetrag (`amount`) des Festpreises. Eine gestellte Anzahlung ist im Rest enthalten und wird über den Baustein „Anzahlung abziehen“ abgesetzt.
          properties:
            mode:
              type: string
              enum:
                - rest
                - percent
                - amount
            percentBp:
              type:
                - integer
                - "null"
              minimum: 1
              maximum: 10000
              description: Nur `percent` — Anteil am Festpreis in Basispunkten (3000 = 30 %).
            amountMicros:
              type:
                - integer
                - "null"
              minimum: 1
              description: Nur `amount` — Nettobetrag in µ€.
    InvoiceDetail:
      description: Rechnung samt Positionen und gerechneten Summen (GET /invoices/{id}).
      allOf:
        - $ref: "#/components/schemas/Invoice"
        - type: object
          required:
            - items
            - totals
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/InvoiceItem"
            totals:
              $ref: "#/components/schemas/InvoiceTotals"
            einvoice_readiness:
              description: |
                Nur bei Entwürfen: Vorprüfung der E-Rechnung (siehe `EInvoiceReadiness`). Bei gestellten Rechnungen `null`.
              oneOf:
                - $ref: "#/components/schemas/EInvoiceReadiness"
                - type: "null"
    InvoiceInput:
      type: object
      description: |
        Schreibbare Felder einer Rechnung. Alle optional (PATCH-Semantik); beim Anlegen füllt der Server nicht gesendete Felder vor. Geld in Mikro-Euro (µ€), Prozentsätze in Basispunkten (1900 = 19,00 %) — nie Fließkomma, damit jeder Wert verlustfrei als JSON-Ganzzahl reist.
      properties:
        customer_id:
          type: string
          format: uuid
          description: |
            PFLICHT beim Anlegen (`POST /invoices`) — ohne Adressat gibt es weder einen Empfänger auf dem Dokument noch eine Adresse, an die der Umsatz in der Rentabilität zurückrollt; fehlt er, antwortet der Server mit 422. Bei `PATCH` ist das Feld wie alle anderen optional (weglassen = Kunde bleibt), darf aber nicht auf `null` gesetzt werden. Dient dem Vorbefüllen und der Auswertung. Die tatsächlich gedruckten Empfängerdaten stehen als Snapshot in den `recipient_*`-Feldern.
            KUNDENWECHSEL PER `PATCH` (nur im Entwurf): Der Server übernimmt vom neuen Kunden `recipient_name` (Rechnungsname vor Kundenname), `recipient_address` samt Einzelfeldern (`recipient_street`, `recipient_address_extra`, `recipient_postal_code`, `recipient_city`, `recipient_country_code`), `recipient_vat_id`, `recipient_email` (erste Versandadresse, sonst Kontakt-E-Mail), `recipient_customer_number`, `buyer_reference` und `einvoice_format`. Felder, die derselbe PATCH selbst mitschickt, gewinnen. Zahlungsziel, Steuer und Positionen bleiben unverändert.
        order_id:
          type:
            - string
            - "null"
          format: uuid
          description: |
            Optionaler Auftragsbezug: der Nettobetrag dieser Rechnung zählt dann im Auftrag als abgerechnet. Der Auftrag muss zum selben Kunden gehören (sonst 422). Bleibt auch an einer festgeschriebenen Rechnung änderbar — er erscheint auf keinem Dokument und ändert keinen Betrag. `null` löst die Zuordnung.
        is_advance:
          type: boolean
          description: Als Anzahlungsrechnung kennzeichnen (nur am Entwurf). Üblicherweise über POST /orders/{id}/advance-invoice angelegt.
        invoice_number:
          type:
            - string
            - "null"
          maxLength: 60
          description: Eigene Nummer, je Workspace eindeutig (sonst 409). Nummern der Form `ENTWURF-…` sind reserviert (422). null/"" = zurück zum Platzhalter, beim Festschreiben vergibt der Nummernkreis die nächste Nummer. Trägt der Entwurf schon eine Nummer aus dem Nummernkreis, lässt sie sich weder ersetzen noch entfernen (409).
        wizard_step:
          type: string
          enum:
            - rahmen
            - inhalte
            - nachweise
            - pruefen
          description: Zuletzt geöffneter Schritt des Rechnungs-Assistenten (reine Oberflächen-Info).
        show_project_column:
          type:
            - boolean
            - "null"
          description: Projekt je Position nur für diese Rechnung zeigen (true) oder ausblenden (false); null = wie die Rechnungsvorlage.
        show_customer_number:
          type:
            - boolean
            - "null"
          description: Kundennummer nur für diese Rechnung in den Kopfdaten zeigen (true) oder ausblenden (false); null = wie die Rechnungsvorlage.
        show_due_date:
          type:
            - boolean
            - "null"
          description: Fälligkeit nur für diese Rechnung in den Kopfdaten zeigen (true) oder ausblenden (false); null = wie die Rechnungsvorlage.
        show_service_period:
          type:
            - boolean
            - "null"
          description: Leistungszeitraum nur für diese Rechnung in den Kopfdaten zeigen (true) oder ausblenden (false); null = wie die Rechnungsvorlage.
        status:
          type: string
          enum:
            - draft
            - final
            - sent
            - paid
            - cancelled
          description: |
            Ab `final` ist die Rechnung inhaltlich gesperrt — eine gestellte Rechnung ist ein Dokument, keine Live-Auswertung.
            `cancelled` wird nicht mehr von Hand gesetzt: der Storno läuft über `POST /invoices/{id}/cancel` und erzeugt dabei den zugehörigen Beleg. Die Antwortfelder `cancels_invoice_id` (nur auf dem Stornobeleg), `cancelled_at` und `cancel_reason` (nur auf dem Original) sind nicht schreibbar.
        title:
          type: string
          minLength: 1
          maxLength: 120
        issue_date:
          type: string
          format: date
          description: Nicht leerbar.
        service_from:
          type:
            - string
            - "null"
          format: date
          description: Zugleich das Fenster, aus dem Positionen und Nachweise erzeugt werden.
        service_to:
          type:
            - string
            - "null"
          format: date
        due_date:
          type:
            - string
            - "null"
          format: date
        payment_terms_days:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 365
        recipient_name:
          type:
            - string
            - "null"
          maxLength: 200
        recipient_address:
          type:
            - string
            - "null"
          maxLength: 1000
        recipient_vat_id:
          type:
            - string
            - "null"
          maxLength: 80
        recipient_email:
          type:
            - string
            - "null"
          maxLength: 320
        sender_name:
          type:
            - string
            - "null"
          maxLength: 200
        sender_address:
          type:
            - string
            - "null"
          maxLength: 1000
        sender_vat_id:
          type:
            - string
            - "null"
          maxLength: 80
        sender_tax_number:
          type:
            - string
            - "null"
          maxLength: 80
        sender_email:
          type:
            - string
            - "null"
          maxLength: 320
        sender_phone:
          type:
            - string
            - "null"
          maxLength: 80
        sender_bank_details:
          type:
            - string
            - "null"
          maxLength: 1000
        recipient_street:
          type:
            - string
            - "null"
          maxLength: 200
          description: Empfängeranschrift in Einzelfeldern (E-Rechnung). Wird ein Baustein geändert und `recipient_address` nicht mitgeschickt, setzt der Server die Textanschrift daraus neu zusammen. Ein Kundenwechsel im Entwurf übernimmt den Empfänger des neuen Kunden.
        recipient_address_extra:
          type:
            - string
            - "null"
          maxLength: 200
        recipient_postal_code:
          type:
            - string
            - "null"
          maxLength: 20
        recipient_city:
          type:
            - string
            - "null"
          maxLength: 200
        recipient_country_code:
          type:
            - string
            - "null"
          pattern: ^[A-Za-z]{2}$
          description: ISO-3166-Alpha-2; wird großgeschrieben gespeichert.
        sender_street:
          type:
            - string
            - "null"
          maxLength: 200
          description: Absenderanschrift in Einzelfeldern. Im Entwurf gewinnt die Rechnungsvorlage, sobald sie eine Anschrift kennt — gepflegt wird der Absender dort.
        sender_address_extra:
          type:
            - string
            - "null"
          maxLength: 200
        sender_postal_code:
          type:
            - string
            - "null"
          maxLength: 20
        sender_city:
          type:
            - string
            - "null"
          maxLength: 200
        sender_country_code:
          type:
            - string
            - "null"
          pattern: ^[A-Za-z]{2}$
        sender_iban:
          type:
            - string
            - "null"
          maxLength: 50
          description: IBAN mit gültiger Prüfsumme (ISO 13616, sonst 422); Leerzeichen erlaubt.
        sender_bic:
          type:
            - string
            - "null"
          maxLength: 20
          description: BIC mit 8 oder 11 Zeichen (sonst 422).
        sender_account_holder:
          type:
            - string
            - "null"
          maxLength: 200
        buyer_reference:
          type:
            - string
            - "null"
          maxLength: 200
          description: Käuferreferenz bzw. Leitweg-ID (BT-10).
        order_reference:
          type:
            - string
            - "null"
          maxLength: 200
          description: Bestellnummer des Kunden (BT-13).
        tax_exemption_reason:
          type:
            - string
            - "null"
          maxLength: 1000
        tax_exemption_code:
          type:
            - string
            - "null"
          pattern: ^[Vv][Aa][Tt][Ee][Xx]-[A-Za-z0-9-]{1,30}$
          description: VATEX-Code (z. B. `VATEX-EU-AE`); wird großgeschrieben gespeichert.
        einvoice_format:
          type: string
          enum:
            - none
            - zugferd
            - xrechnung
          description: E-Rechnungsformat dieser Rechnung (Standard aus dem Kunden).
        intro_text:
          type:
            - string
            - "null"
          maxLength: 5000
        closing_text:
          type:
            - string
            - "null"
          maxLength: 5000
        payment_note:
          type:
            - string
            - "null"
          maxLength: 5000
        tax_note:
          type:
            - string
            - "null"
          maxLength: 5000
          description: |
            Leer → automatisch der passende Rechtshinweis zur gewählten `tax_mode` (z. B. § 19 UStG bei Kleinunternehmern).
        currency:
          type: string
          enum:
            - EUR
            - USD
            - CHF
        tax_mode:
          type: string
          enum:
            - standard
            - small_business
            - reverse_charge
            - exempt
          description: Nur `standard` weist Umsatzsteuer aus; die übrigen tragen einen Rechtshinweis.
        tax_rate_bp:
          type: integer
          minimum: 0
          maximum: 10000
          description: Basispunkte (1900 = 19 %). null oder "" → 422 (nie still 0 %).
        discount_bp:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 10000
          description: |
            Nachlass auf die Zwischensumme. Wird proportional auf steuerpflichtige und steuerfreie Positionen verteilt, damit die Steuer-Bemessungsgrundlage stimmt.
        discount_label:
          type:
            - string
            - "null"
          maxLength: 80
        layout:
          oneOf:
            - $ref: "#/components/schemas/InvoiceLayoutConfig"
            - type: "null"
          description: Rechnungslayout; null = Standard. Nur am Entwurf änderbar.
        source_project_ids:
          type:
            - array
            - "null"
          maxItems: 200
          items:
            type: string
            format: uuid
          description: |
            Beschränkt Positions- und Nachweis-Erzeugung. Leer/null = alle Projekte des Kunden bzw. des Workspaces.
    InvoiceItem:
      type: object
      required:
        - id
        - invoice_id
        - sort_order
        - kind
        - title
        - description
        - project_id
        - project_name
        - service_from
        - service_to
        - quantity_milli
        - unit
        - unit_price_micros
        - amount_micros
        - taxable
        - source
      properties:
        id:
          type: string
          format: uuid
        invoice_id:
          type: string
          format: uuid
        sort_order:
          type: integer
        kind:
          type: string
          enum:
            - time
            - ai_tokens
            - ai_cost
            - ai_runtime
            - expense
            - manual
            - discount
            - advance
            - fixed_price
            - text
        title:
          type: string
        description:
          type:
            - string
            - "null"
        project_id:
          type:
            - string
            - "null"
          format: uuid
        project_name:
          type:
            - string
            - "null"
        service_from:
          type:
            - string
            - "null"
          format: date
        service_to:
          type:
            - string
            - "null"
          format: date
        quantity_milli:
          type: integer
          description: Menge in Tausendstel (12500 = 12,5).
        unit:
          type: string
        unit_code:
          type:
            - string
            - "null"
          description: Einheitencode nach UN/ECE Rec. 20 (HUR, MIN, DAY, MON, C62 …) für die E-Rechnung; null = aus `unit` abgeleitet (Positionen von vor der E-Rechnung).
        unit_price_micros:
          type: integer
        amount_micros:
          type: integer
        taxable:
          type: boolean
        tax_micros:
          type:
            - integer
            - "null"
          description: "Fest vorgegebener Steuerbetrag in µ€ — nur beim Anzahlungsabzug (`advance`) einer Endrechnung: die Steuer, die die Anzahlungsrechnung ausgewiesen hat, negativ (im Storno positiv). Solche Abzüge zählen nicht zur Leistung, sondern werden nach der Steuer abgesetzt. null = Steuer aus der Summe (alle übrigen Positionen; Abzüge vor dem 10.10.2026). Nur lesbar."
        source:
          type:
            - object
            - "null"
          additionalProperties: true
          description: Herkunft einer erzeugten Position (Leistungs-IDs, Aufteilung je Projekt). Beim Anzahlungsabzug zusätzlich `advance_invoice_id`, `advance_invoice_number`, `advance_issue_date`, `advance_tax_mode`, `advance_tax_rate_bp`.
    InvoiceItemInput:
      type: object
      description: |
        Schreibbare Felder einer Rechnungsposition. Mengen in Tausendstel der Einheit (12500 = 12,5 Stunden), Geld in µ€.
      properties:
        kind:
          type: string
          enum:
            - time
            - ai_tokens
            - ai_cost
            - ai_runtime
            - manual
            - text
          default: manual
          description: |
            `text` ist eine reine Zwischenüberschrift ohne Betrag und fließt nicht in die Summen ein. `ai_runtime` = aktive KI-Laufzeit × Stundensatz. `expense` (weiterbelastete Einzelkosten) entsteht nur aus dem Baustein Projektkosten, `discount` (Rabatt) nur aus dem Baustein Rabatt, `advance` (Anzahlungsabzug) nur aus dem Baustein „Anzahlung abziehen“, `fixed_price` (Festpreis) nur aus dem Baustein „Festpreis“ — alle vier sind hier nicht zulässig.
        title:
          type: string
          minLength: 1
          maxLength: 300
        description:
          type:
            - string
            - "null"
          maxLength: 5000
        project_id:
          type:
            - string
            - "null"
          format: uuid
        project_name:
          type:
            - string
            - "null"
          maxLength: 200
          description: Snapshot des Projektnamens — bleibt lesbar, wenn das Projekt gelöscht wird.
        service_from:
          type:
            - string
            - "null"
          format: date
        service_to:
          type:
            - string
            - "null"
          format: date
        quantity_milli:
          type: integer
          minimum: -1000000000
          maximum: 1000000000
          description: Menge in Tausendstel (12500 = 12,5).
        unit:
          type: string
          maxLength: 40
        unit_code:
          type:
            - string
            - "null"
          pattern: ^[A-Za-z0-9]{1,3}$
          description: Einheitencode nach UN/ECE Rec. 20. Ohne Angabe leitet der Server ihn aus `unit` ab (Stunden → HUR, Minuten → MIN, Tage → DAY, Monat → MON, sonst C62) — beim Anlegen immer, beim Ändern, sobald sich `unit` ändert.
        unit_price_micros:
          type: integer
          minimum: -1000000000000
          maximum: 1000000000000
        amount_micros:
          type: integer
          minimum: -1000000000000
          maximum: 1000000000000
          description: |
            Optional. Fehlt der Wert, rechnet der Server `quantity_milli × unit_price_micros ÷ 1000` — auf einer Rechnung muss Menge × Einzelpreis den Betrag ergeben.
        taxable:
          type: boolean
          default: true
        sort_order:
          type: integer
          minimum: 0
          maximum: 100000
          description: Ohne Angabe ans Ende.
    InvoiceLayout:
      type: object
      description: Eigenes Layout (Tabelle `invoice_layouts`) — für Rechnung oder Leistungsnachweis.
      required:
        - id
        - workspace_id
        - document_type
        - name
        - source_html
        - source_css
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        document_type:
          type: string
          enum:
            - invoice
            - statement
          description: Dokumentart; beim Anlegen festgelegt und danach unveränderlich.
        name:
          type: string
        source_html:
          type: string
          description: HTML mit Liquid-Platzhaltern.
        source_css:
          type: string
        version:
          type: integer
          description: Zählt bei jeder Quelltextänderung hoch.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    InvoiceLayoutSummary:
      type: object
      required:
        - id
        - document_type
        - name
        - version
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        document_type:
          type: string
          enum:
            - invoice
            - statement
        name:
          type: string
        version:
          type: integer
        updated_at:
          type: string
          format: date-time
    InvoiceLayoutInput:
      type: object
      description: Felder in camelCase. Beim Anlegen fehlt Quelltext → leer.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        sourceHtml:
          type: string
          maxLength: 200000
        sourceCss:
          type: string
          maxLength: 200000
        documentType:
          type: string
          enum:
            - invoice
            - statement
          description: Nur beim Anlegen (Standard `invoice`; auch `document_type`). Beim Ändern nur der bestehende Wert erlaubt — die Art eines Layouts lässt sich nicht ändern (422).
    InvoiceLayoutConfig:
      type: object
      description: "Layout einer Rechnung (jsonb `invoices.layout`, snake_case). Gelesen werden nur bekannte Schlüssel; ungültige Werte fallen einzeln auf ihren Standard zurück. Beim Schreiben wird das Objekt vollständig aufgelöst — nicht gesendete Schlüssel erhalten ihren Standard; `layout: null` setzt alles zurück."
      properties:
        design:
          type:
            - string
            - "null"
          enum:
            - hausstil
            - din
            - manufaktur
            - raster
            - seitenleiste
            - beleg
            - eigenes
            - null
          description: Grundaufbau; `eigenes` rendert das Layout `custom_layout_id`. null = Rechnung von vor den Rechnungslayouts.
        custom_layout_id:
          type:
            - string
            - "null"
          format: uuid
          description: "Eigenes Layout aus /invoice-layouts (nur bei `design: eigenes`)."
        font:
          type:
            - string
            - "null"
          description: Schrift-Schlüssel (z. B. arimo, source-serif, cormorant, dm-sans, archivo, bricolage, jetbrains-mono); null = Standard des Layouts.
        show_girocode:
          type: boolean
          description: GiroCode (EPC-QR) zum Bezahlen auf die Rechnung setzen.
        options:
          type: object
          description: Einstellungen des gewählten Layouts (z. B. `kopf`, `summenfeld`, `falzmarken`, `ansprechpartner`, `linien`, `titel`, `betrag_oben`, `seite`, `toenung`) — flache Schlüssel `^[a-z][a-z0-9_]{0,39}$` mit Text (≤ 40 Zeichen) oder Wahrheitswert, höchstens 30.
          additionalProperties:
            type:
              - string
              - boolean
        accent_color:
          type:
            - string
            - "null"
          pattern: ^#[0-9a-fA-F]{6}$
          description: null = Akzentfarbe des Workspace-Brandings.
        show_logo:
          type: boolean
        header_style:
          type: string
          enum:
            - band
            - minimal
        show_position_numbers:
          type: boolean
        show_project_column:
          type: boolean
        show_period_column:
          type: boolean
        show_unit_price:
          type: boolean
        show_item_notes:
          type: boolean
        show_domestic_country:
          type: boolean
          description: Land in der Empfängeranschrift auch bei Inlandsanschriften (Standard false).
        show_customer_number:
          type: boolean
          description: Kundennummer in den Kopfdaten (Standard true).
        show_due_date:
          type: boolean
          description: Fälligkeit und Zahlungsziel in den Kopfdaten (Standard true). Der Platzhalter `{faellig}` in den Texten wird unabhängig davon ersetzt.
        show_service_period:
          type: boolean
          description: Leistungszeitraum in den Kopfdaten (Standard true). Pflichtangabe nach § 14 UStG — wer ihn ausblendet, sollte ihn z. B. über `{zeitraum}` im Einleitungstext nennen.
        zebra:
          type: boolean
        footer_text:
          type:
            - string
            - "null"
          maxLength: 300
          description: null = Fußzeile des Workspace-Brandings.
        custom_source:
          type: object
          description: Nur lesend — beim Festschreiben eingefrorener Quelltext eines eigenen Layouts.
          properties:
            html:
              type: string
            css:
              type: string
    InvoicePayment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        invoice_id:
          type: string
          format: uuid
        paid_on:
          type: string
          format: date
        amount_micros:
          type: integer
        note:
          type:
            - string
            - "null"
        transfer_id:
          type:
            - string
            - "null"
          format: uuid
          description: |
            Sammelzahlung: Anteile EINES Kontoeingangs, der auf mehrere Rechnungen verteilt wurde (`POST /payments`), tragen dieselbe Kennung. `null` = Einzelzahlung.
        created_at:
          type: string
          format: date-time
    InvoicePaymentInput:
      type: object
      description: |
        Body von `POST` (Pflicht: `paid_on`, `amount_micros`) und `PATCH` (alle Felder optional) auf `/invoices/{id}/payments`.
      properties:
        paid_on:
          type: string
          format: date
          description: Eingangstag laut Kontoauszug. Nicht in der Zukunft.
        amount_micros:
          type: integer
          minimum: 1
          maximum: 1000000000000000
          description: |
            Eingegangener Bruttobetrag in Mikro-Einheiten der Rechnungswährung (1 € = 1.000.000). Immer positiv — Rückzahlungen laufen über den Storno.
        note:
          type:
            - string
            - "null"
          maxLength: 500
          description: Verwendungszweck, Zahlungsweg oder Bankreferenz. Leer = keine Notiz.
    InvoicePaymentCreate:
      description: Body von `POST /invoices/{id}/payments` — Eingangstag und Betrag sind Pflicht.
      allOf:
        - $ref: "#/components/schemas/InvoicePaymentInput"
        - type: object
          required:
            - paid_on
            - amount_micros
    PaymentTransferCreate:
      type: object
      description: |
        Body von `POST /payments` — eine Zahlung (z. B. eine Überweisung), aufgeteilt auf eine oder mehrere Rechnungen.
      required:
        - paid_on
        - allocations
      properties:
        paid_on:
          type: string
          format: date
          description: Eingangstag laut Kontoauszug. Nicht in der Zukunft.
        note:
          type:
            - string
            - "null"
          maxLength: 500
          description: Verwendungszweck, Zahlungsweg oder Bankreferenz — gilt für alle Anteile.
        allocations:
          type: array
          minItems: 1
          maxItems: 50
          description: Anteil je Rechnung; jede Rechnung höchstens einmal, alle in derselben Währung.
          items:
            type: object
            required:
              - invoice_id
              - amount_micros
            properties:
              invoice_id:
                type: string
                format: uuid
              amount_micros:
                type: integer
                minimum: 1
                maximum: 1000000000000000
                description: Anteil dieser Rechnung, brutto in Mikro-Einheiten (1 € = 1.000.000).
    PaymentTransferResult:
      type: object
      required:
        - transfer_id
        - total_micros
        - data
        - invoices
      properties:
        transfer_id:
          type:
            - string
            - "null"
          format: uuid
          description: Gemeinsame Kennung der Anteile; `null`, wenn nur eine Rechnung beteiligt ist.
        total_micros:
          type: integer
          description: Summe aller Anteile.
        data:
          type: array
          items:
            $ref: "#/components/schemas/InvoicePayment"
        invoices:
          type: array
          description: Status und Zahlungsstand jeder beteiligten Rechnung nach dem Abgleich.
          items:
            type: object
            required:
              - invoice_id
              - invoice_status
              - summary
            properties:
              invoice_id:
                type: string
                format: uuid
              invoice_status:
                type: string
                enum:
                  - draft
                  - final
                  - sent
                  - paid
                  - cancelled
              summary:
                $ref: "#/components/schemas/InvoicePaymentSummary"
    InvoicePaymentResult:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/InvoicePayment"
        invoice_status:
          type: string
          enum:
            - draft
            - final
            - sent
            - paid
            - cancelled
          description: Gespeicherter Status der Rechnung nach dem Statusabgleich.
        summary:
          $ref: "#/components/schemas/InvoicePaymentSummary"
    InvoicePaymentSummary:
      type: object
      description: Zahlungsstand einer Rechnung (gerechnet, nicht gespeichert).
      required:
        - gross_micros
        - paid_micros
        - outstanding_micros
        - overpaid_micros
        - display_status
        - is_overdue
      properties:
        gross_micros:
          type: integer
          description: Bruttobetrag aus den Positionen.
        paid_micros:
          type: integer
          description: Summe der erfassten Zahlungen.
        outstanding_micros:
          type: integer
          description: |
            Noch offen (nie negativ). 0 bei Entwurf, Storno, Stornobeleg und `paid` — auch einer von Hand bezahlten Rechnung mit geringeren Zahlungen.
        overpaid_micros:
          type: integer
          description: Mehr eingegangen als gefordert (sonst 0).
        display_status:
          type: string
          enum:
            - draft
            - final
            - sent
            - partial
            - overdue
            - paid
            - cancelled
          description: |
            Gespeicherter Status plus die abgeleiteten Zustände `partial` (Teilzahlung, Rest offen) und `overdue` (Fälligkeit vor heute, Betrag offen; hat Vorrang vor `partial`).
        is_overdue:
          type: boolean
    InvoicePreview:
      type: object
      description: |
        Ergebnis von `GET /invoices/{id}/preview` — was beim Erzeugen entstünde, ohne dass etwas geschrieben wurde.
      required:
        - items
        - by_project
        - net_micros
        - replace
        - kept_items
        - evaluated
      properties:
        items:
          type: array
          description: |
            Die vorgeschlagenen Positionen. Bewusst OHNE `source`: die Herkunfts-IDs einer Sammelposition sind bis zu 2000 UUIDs und für eine Vorschau ohne jeden Wert.
          items:
            $ref: "#/components/schemas/InvoiceItemInput"
        by_project:
          type: array
          description: |
            Umsatz je Projekt (netto, Nachlass anteilig abgezogen) — zeigt sofort, wohin der Betrag rollt. `project_id: null` = Position ohne Projektbezug.
          items:
            type: object
            properties:
              project_id:
                type:
                  - string
                  - "null"
                format: uuid
              project_name:
                type:
                  - string
                  - "null"
              amount_micros:
                type: integer
              seconds:
                type: integer
              tokens:
                type: integer
        net_micros:
          type: integer
          description: |
            Nettobetrag der vorgeschlagenen Rechnung (Zwischensumme − Nachlass). Entspricht exakt der Summe über `by_project` — das ist die zentrale Invariante der Schlüsselung.
        replace:
          type: boolean
        kept_items:
          type: integer
          description: Anzahl bestehender Positionen, die stehen blieben.
        evaluated:
          type: object
          properties:
            timers:
              type: integer
            sessions:
              type: integer
            project_ids:
              type: array
              items:
                type: string
                format: uuid
            dropped_project_ids:
              type: array
              items:
                type: string
                format: uuid
            entity_ids_truncated:
              type: boolean
              description: |
                true = eine vorgeschlagene Position führt mehr Leistungen, als ihre Herkunftsliste fasst; für sie entstünde kein Sperrvermerk gegen Doppelfakturierung. Die Vorschau ist die letzte Gelegenheit, den Zuschnitt vorher zu ändern.
            skipped_billed:
              $ref: "#/components/schemas/SkippedBilled"
    InvoiceRecipient:
      type: object
      required:
        - email
        - description
      properties:
        email:
          type: string
          format: email
          maxLength: 320
        description:
          type:
            - string
            - "null"
          maxLength: 200
    InvoiceStatementInput:
      type: object
      description: |
        Body von `POST /invoices/{id}/statements`. Entweder `statement_id` ODER `create: true` — nie beides.
      properties:
        statement_id:
          type: string
          format: uuid
          description: Bestehenden Nachweis verknüpfen. Muss demselben Kunden gehören wie die Rechnung.
        create:
          type: boolean
          default: false
          description: |
            true = neuen Leistungsnachweis (`kind: proof`) aus der Rechnung erzeugen und verknüpfen; seine Auswahl folgt der Rechnung.
        label:
          type:
            - string
            - "null"
          maxLength: 60
          description: |
            Beschriftung im Dokument („Anlage 1“). null/leer = die Anzeige nummeriert selbst durch.
        sort_order:
          type: integer
          minimum: 0
          maximum: 100000
          description: Ohne Angabe fällt die Anlage ans Ende der bestehenden.
    InvoiceStatementLink:
      type: object
      description: Eine Verknüpfung samt ihres Nachweises (ohne Snapshot).
      properties:
        id:
          type: string
          format: uuid
          description: ID der VERKNÜPFUNG (nicht des Nachweises).
        workspace_id:
          type: string
          format: uuid
        invoice_id:
          type: string
          format: uuid
        statement_id:
          type: string
          format: uuid
        label:
          type:
            - string
            - "null"
        display_label:
          type: string
          description: Gespeicherte Beschriftung, sonst die laufende Nummerierung („Anlage 2“).
        sort_order:
          type: integer
        created_at:
          type: string
          format: date-time
        statement:
          $ref: "#/components/schemas/Statement"
    InvoiceTemplate:
      type: object
      description: 'Rechnungsvorlage des Workspaces (camelCase wie der Typ `InvoiceTemplate`). Entwürfe lesen Absender, Bank, Steuer-IDs und Texte live aus der Vorlage; beim Festschreiben werden sie als Schnappschuss in die Rechnung kopiert. Texte dürfen die Platzhalter `{zeitraum}`, `{faellig}`, `{nummer}` und `{kunde}` enthalten. Beim Speichern: fehlende Felder bleiben wie gespeichert, Textfelder null/"" = leeren. Die Felder `statement*` bilden den Bereich „Leistungsnachweis“: eigener Nummernkreis (vergeben beim Festschreiben eines Nachweises), Einleitung/Schlusstext (Platzhalter `{zeitraum}`, `{nummer}`, `{kunde}`), Layout und Unterschriftsfeld.'
      properties:
        senderName:
          type:
            - string
            - "null"
          maxLength: 200
        senderStreet:
          type:
            - string
            - "null"
          maxLength: 200
        senderAddressExtra:
          type:
            - string
            - "null"
          maxLength: 200
        senderPostalCode:
          type:
            - string
            - "null"
          maxLength: 20
        senderCity:
          type:
            - string
            - "null"
          maxLength: 200
        senderCountry:
          type:
            - string
            - "null"
          maxLength: 80
          description: ISO-Ländercode oder Ländername; Deutschland erscheint nicht in der Anschrift.
        senderEmail:
          type:
            - string
            - "null"
          maxLength: 320
          format: email
        senderPhone:
          type:
            - string
            - "null"
          maxLength: 80
        senderWebsite:
          type:
            - string
            - "null"
          maxLength: 200
        vatId:
          type:
            - string
            - "null"
          maxLength: 80
        taxNumber:
          type:
            - string
            - "null"
          maxLength: 80
        registerType:
          type:
            - string
            - "null"
          enum:
            - HRB
            - HRA
            - GnR
            - PR
            - VR
            - GsR
            - null
        registerNumber:
          type:
            - string
            - "null"
          maxLength: 40
        registerCourt:
          type:
            - string
            - "null"
          maxLength: 120
        managingDirectors:
          type:
            - string
            - "null"
          maxLength: 500
          description: Geschäftsführung, eine Person je Zeile.
        signerName:
          type:
            - string
            - "null"
          maxLength: 200
        greetingText:
          type:
            - string
            - "null"
          maxLength: 200
          description: Grußformel unter dem Schlusstext (Standard „Mit freundlichen Grüßen“).
        defaultTaxMode:
          type: string
          enum:
            - standard
            - small_business
            - reverse_charge
            - exempt
        defaultTaxRateBp:
          type: integer
          minimum: 0
          maximum: 10000
          description: Bei `standard` muss er über 0 liegen (422).
        bankAccountHolder:
          type:
            - string
            - "null"
          maxLength: 200
          description: Bankverbindung strukturiert — Quelle der Zahlungsanweisung der E-Rechnung (`sender_iban`, `sender_bic`, `sender_account_holder` an der Rechnung).
        bankIban:
          type:
            - string
            - "null"
          maxLength: 50
          description: Prüfsumme wird geprüft; gespeichert ohne Leerraum, großgeschrieben.
        bankBic:
          type:
            - string
            - "null"
          maxLength: 20
          description: 8 oder 11 Zeichen.
        bankName:
          type:
            - string
            - "null"
          maxLength: 200
        numberPrefix:
          type: string
          maxLength: 20
          pattern: ^[\p{L}\p{N}._/-]*$
          description: Darf leer sein; nur Buchstaben, Ziffern und . _ / -.
        numberFormat:
          type: string
          enum:
            - year
            - year_month
            - plain
          description: "`year` → RE-2026-0001, `year_month` → RE-2026-09-0001, `plain` → RE-0001."
        numberPadding:
          type: integer
          minimum: 1
          maximum: 8
        numberNext:
          type: integer
          minimum: 1
          maximum: 99999999
          description: Nächste laufende Nummer.
        numberYear:
          type:
            - integer
            - "null"
          readOnly: true
          description: Nur lesend — Jahr, dem der Zähler gehört (setzt der Server).
        defaultPaymentTermsDays:
          type: integer
          minimum: 0
          maximum: 365
        introText:
          type:
            - string
            - "null"
          maxLength: 5000
        closingText:
          type:
            - string
            - "null"
          maxLength: 5000
        layout:
          $ref: "#/components/schemas/InvoiceTemplateLayout"
        statementLayout:
          $ref: "#/components/schemas/InvoiceTemplateLayout"
          description: "Layout des Leistungsnachweises (gleiche Schlüssel wie `layout`, wird ebenso VOLLSTÄNDIG ersetzt). Abweichend darf `design` hier null sein: dann gilt das Layout der Rechnung (bei einem eigenen Rechnungslayout der Standard-Grundaufbau mit Farbe, Schrift und Logo der Rechnung). `eigenes` verlangt ein Layout mit `document_type: statement` (422 sonst)."
        statementIntroText:
          type:
            - string
            - "null"
          maxLength: 5000
          description: Einleitung des Nachweises; ein Nachweis mit eigener Einleitung übersteuert sie.
        statementClosingText:
          type:
            - string
            - "null"
          maxLength: 5000
        statementNumberPrefix:
          type: string
          maxLength: 20
          pattern: ^[\p{L}\p{N}._/-]*$
          description: Präfix des Nachweis-Nummernkreises (Standard `NW`).
        statementNumberFormat:
          type: string
          enum:
            - year
            - year_month
            - plain
          description: Wie `numberFormat` (Standard `year` → NW-2026-0001).
        statementNumberPadding:
          type: integer
          minimum: 1
          maximum: 8
        statementNumberNext:
          type: integer
          minimum: 1
          maximum: 99999999
          description: Nächste laufende Nachweisnummer. Ein noch nie benutzter Kreis zeigt beim Lesen die Nummer über den bisherigen `NW-<Jahr>-<n>`-Nummern.
        statementNumberYear:
          type:
            - integer
            - "null"
          readOnly: true
          description: Nur lesend — Jahr, dem der Nachweis-Zähler gehört (null = noch nie benutzt).
        statementSignature:
          type: boolean
          description: Unterschriftsfeld „Leistung bestätigt“ am Ende des Nachweises (Standard aus; je Nachweis über `content_config.signature` übersteuerbar).
        statementHeadFields:
          $ref: "#/components/schemas/StatementHeadFields"
        reusableNumber:
          readOnly: true
          description: Nur lesend, nicht speicherbar — eine bei einer Rücknahme zurückgegebene laufende Rechnungsnummer, die der Kreis als nächste wieder vergibt (vor dem Zähler), sonst null. Fehlt ohne gespeicherte Vorlage.
          oneOf:
            - $ref: "#/components/schemas/ReusableNumber"
            - type: "null"
        reusableStatementNumber:
          readOnly: true
          description: Wie `reusableNumber`, für den Nachweis-Nummernkreis.
          oneOf:
            - $ref: "#/components/schemas/ReusableNumber"
            - type: "null"
    ReusableNumber:
      type: object
      required:
        - sequence
        - year
      description: Zur Wiedervergabe vorgemerkte laufende Nummer eines Nummernkreises. Vergeben wird sie in der dann gültigen Schreibweise (Präfix, Stellen, Monat).
      properties:
        sequence:
          type: integer
          minimum: 1
        year:
          type:
            - integer
            - "null"
          description: Jahr des Kreises; null beim Format `plain`.
    InvoiceTemplateLayout:
      type: object
      description: Layout der Rechnungsvorlage (camelCase; snake_case wie in `InvoiceLayoutConfig` wird beim Schreiben ebenfalls verstanden). Beim Speichern wird das gesendete Layout VOLLSTÄNDIG übernommen — nicht gesendete Schlüssel fallen auf den Standard zurück, daher immer das ganze Objekt senden (vorher per GET lesen). Die Vorlage hat immer einen Grundaufbau (Standard `hausstil`); `eigenes` ohne `customLayoutId` fällt auf ihn zurück, ein fremdes Layout → 422.
      properties:
        design:
          type: string
          enum:
            - hausstil
            - din
            - manufaktur
            - raster
            - seitenleiste
            - beleg
            - eigenes
        customLayoutId:
          type:
            - string
            - "null"
          format: uuid
        font:
          type:
            - string
            - "null"
        showGiroCode:
          type: boolean
        options:
          type: object
          additionalProperties:
            type:
              - string
              - boolean
        accentColor:
          type:
            - string
            - "null"
          pattern: ^#[0-9a-fA-F]{6}$
        showLogo:
          type: boolean
        headerStyle:
          type: string
          enum:
            - band
            - minimal
        showPositionNumbers:
          type: boolean
        showProjectColumn:
          type: boolean
        showPeriodColumn:
          type: boolean
        showUnitPrice:
          type: boolean
        showItemNotes:
          type: boolean
        showDomesticCountry:
          type: boolean
        showCustomerNumber:
          type: boolean
        showDueDate:
          type: boolean
        showServicePeriod:
          type: boolean
        zebra:
          type: boolean
        footerText:
          type:
            - string
            - "null"
          maxLength: 300
    InvoiceTemplateResult:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          $ref: "#/components/schemas/InvoiceTemplate"
        meta:
          type: object
          required:
            - stored
          properties:
            stored:
              type: boolean
              description: Ob schon eine Vorlage gespeichert ist (sonst Standardwerte).
    InvoiceTotals:
      type: object
      description: Aus den Positionen gerechnete Beträge (nie gespeichert), in µ€. Eine Endrechnung nach Anzahlung (§ 14 Abs. 5 Satz 2 UStG) weist die gesamte Leistung aus (`service*`) und setzt die Anzahlungen darunter ab (`prepaid*`); `net/tax/grossMicros` sind dann der Rest, den diese Rechnung selbst beiträgt. Ohne Anzahlung sind `service*` und `net/tax/gross` gleich und `prepaid*` 0.
      required:
        - subtotalMicros
        - taxableSubtotalMicros
        - discountMicros
        - taxableDiscountMicros
        - taxBaseMicros
        - serviceNetMicros
        - serviceTaxMicros
        - serviceGrossMicros
        - prepaidNetMicros
        - prepaidTaxMicros
        - prepaidMicros
        - netMicros
        - taxMicros
        - grossMicros
        - amountDueMicros
      properties:
        subtotalMicros:
          type: integer
          description: Summe der betragstragenden Positionen der Leistung vor Nachlass (ohne Anzahlungsabzüge).
        taxableSubtotalMicros:
          type: integer
        discountMicros:
          type: integer
        taxableDiscountMicros:
          type: integer
        taxBaseMicros:
          type: integer
          description: Bemessungsgrundlage der Umsatzsteuer der Leistung.
        serviceNetMicros:
          type: integer
          description: Nettobetrag der Leistung (Zwischensumme − Nachlass).
        serviceTaxMicros:
          type: integer
          description: Umsatzsteuer der Leistung (nur bei `tax_mode` standard).
        serviceGrossMicros:
          type: integer
          description: Gesamtbetrag der Leistung (Netto + Steuer).
        prepaidNetMicros:
          type: integer
          description: Netto der abgezogenen Anzahlungen (Endrechnung; im Storno negativ).
        prepaidTaxMicros:
          type: integer
          description: Von den Anzahlungsrechnungen ausgewiesene und hier abgezogene Steuer.
        prepaidMicros:
          type: integer
          description: Brutto der abgezogenen Anzahlungen (BT-113 „Paid amount“).
        netMicros:
          type: integer
          description: Netto, das diese Rechnung beiträgt (Leistung − Anzahlungen) — Umsatz und Auftragsstand.
        taxMicros:
          type: integer
          description: Umsatzsteuer der Leistung abzüglich der Steuer der Anzahlungen.
        grossMicros:
          type: integer
          description: Zahlbetrag (Netto + Steuer); bei einer Endrechnung der noch zu zahlende Rest.
        amountDueMicros:
          type: integer
          description: Gleich `grossMicros` (BT-115 „Amount due for payment“).
    MatchSuggestion:
      type: object
      required:
        - id
        - tool
        - started_at
        - overlap_seconds
      description: Eine zeitlich überlappende, projekt-kompatible KI-Session als Timer-Match.
      properties:
        id:
          type: string
          format: uuid
        tool:
          type: string
        folder_path:
          type:
            - string
            - "null"
        activity_context:
          type:
            - string
            - "null"
        model:
          type:
            - string
            - "null"
        started_at:
          type: string
          format: date-time
        ended_at:
          type:
            - string
            - "null"
          format: date-time
        project_id:
          type:
            - string
            - "null"
          format: uuid
        overlap_seconds:
          type: integer
          description: Sekunden der Überlappung mit dem Timer-Zeitfenster.
    ModelAlias:
      type: object
      required:
        - id
        - workspace_id
        - tool
        - raw_model
        - canonical_model
        - created_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type:
            - string
            - "null"
          format: uuid
          description: null = globaler Alias (nur lesbar).
        tool:
          type: string
        raw_model:
          type: string
        canonical_model:
          type: string
        created_at:
          type: string
          format: date-time
    ModelPrice:
      type: object
      required:
        - id
        - provider
        - model
        - currency
        - input_price_micros
        - output_price_micros
        - valid_from
        - valid_to
      description: |
        Preisversion eines Modells. Einheit: Mikro-Einheiten der Währung `currency` pro 1 Mio. Token. Die Bewertung rechnet USD-Preise mit dem EZB-Referenzkurs vom (Berliner) Tag des Session-Starts in Euro um; an Tagen ohne Kurs gilt der letzte davor.
      properties:
        id:
          type: string
          format: uuid
        provider:
          type: string
        model:
          type: string
        display_name:
          type:
            - string
            - "null"
        currency:
          type: string
          enum:
            - EUR
            - USD
        input_price_micros:
          type: integer
        output_price_micros:
          type: integer
        cache_read_price_micros:
          type:
            - integer
            - "null"
          description: µ€ je 1 Mio. gecacht gelesener Input-Token.
        cache_write_price_micros:
          type:
            - integer
            - "null"
          description: µ€ je 1 Mio. Cache-Erzeugungs-Token.
        long_context_threshold_tokens:
          type:
            - integer
            - "null"
          enum:
            - 100000
            - 200000
            - 272000
            - null
          description: |
            Kontext-Schwelle je Anfrage (Input + Cache-Lesen + Cache-Erzeugung). Überschreitet eine Anfrage sie, läuft die GANZE Anfrage zu den Long-Context-Preisen. null = kein Aufschlag (voller Kontext zum Standardpreis).
        long_context_input_price_micros:
          type:
            - integer
            - "null"
          description: Long-Context-Preis je 1 Mio. frischer Input-Token (Währung wie `currency`).
        long_context_output_price_micros:
          type:
            - integer
            - "null"
        long_context_cache_read_price_micros:
          type:
            - integer
            - "null"
          description: Fehlt er, gilt der Long-Context-Input-Preis.
        long_context_cache_write_price_micros:
          type:
            - integer
            - "null"
          description: Fehlt er, gilt der Long-Context-Input-Preis (Claude-TTL-Anteile 1,25×/2× Input).
        valid_from:
          type: string
          format: date-time
        valid_to:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Order:
      type: object
      required:
        - id
        - workspace_id
        - customer_id
        - project_ids
        - project_id
        - title
        - ordered_on
        - ends_on
        - volume_micros
        - time_budget_seconds
        - currency
        - include_non_billable
        - billing_model
        - hourly_rate_cents
        - allow_overbooking
        - status
        - created_at
        - updated_at
        - deleted_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        customer_id:
          type: string
          format: uuid
        project_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Projekte, auf die der Auftrag zählt; leer = alle Projekte des Kunden. Mehrere Projekte teilen sich das Kontingent.
        project_id:
          type:
            - string
            - "null"
          format: uuid
          description: 'Kompatibilität: das Projekt, wenn `project_ids` genau eines enthält, sonst null. null heißt also nicht zwingend „alle Projekte" — maßgeblich ist `project_ids`.'
        title:
          type: string
        order_number:
          type:
            - string
            - "null"
        description:
          type:
            - string
            - "null"
        ordered_on:
          type: string
          format: date
        ends_on:
          type:
            - string
            - "null"
          format: date
        volume_micros:
          type:
            - integer
            - "null"
          format: int64
        time_budget_seconds:
          type:
            - integer
            - "null"
          format: int64
        currency:
          type: string
        include_non_billable:
          type: boolean
        billing_model:
          type: string
          enum:
            - time_and_material
            - fixed_price
        hourly_rate_cents:
          type:
            - integer
            - "null"
          description: Vereinbarter Stundensatz in Euro-Cent (Kalkulation Zeitkontingent × Satz).
        allow_overbooking:
          type: boolean
          description: Darf der Auftrag mehr Arbeit aufnehmen, als sein Kontingent fasst? Greift erst, wenn kein anderer passender Auftrag mehr frei ist (Auftragsbindung mit Überlauf).
        status:
          type: string
          enum:
            - open
            - completed
            - cancelled
        notes:
          type:
            - string
            - "null"
        buyer_reference:
          type:
            - string
            - "null"
          description: Käuferreferenz bzw. Leitweg-ID (EN 16931 BT-10) für Rechnungen zu diesem Auftrag. Gilt in der Rechnung, solange dort kein eigener Wert steht — vor dem Wert des Kunden.
        order_reference:
          type:
            - string
            - "null"
          description: Bestellnummer des Kunden (EN 16931 BT-13) für Rechnungen zu diesem Auftrag. Gilt in der Rechnung, solange dort keine eigene steht.
        advance_amount_micros:
          type:
            - integer
            - "null"
          format: int64
          description: Vereinbarte Anzahlung als fester Nettobetrag in µ€ (null = keine bzw. Prozent).
        advance_percent_bp:
          type:
            - integer
            - "null"
          description: Vereinbarte Anzahlung als Prozentsatz des Volumens in Basispunkten (3000 = 30 %).
        advance_due_on:
          type:
            - string
            - "null"
          format: date
          description: Fälligkeit der Anzahlung; null = sofort mit der Beauftragung (`ordered_on`).
        advance:
          $ref: "#/components/schemas/OrderAdvance"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - "null"
          format: date-time
          description: Gesetzt = archiviert.
        progress:
          $ref: "#/components/schemas/OrderProgress"
        window:
          $ref: "#/components/schemas/OrderWindow"
    OrderAdvance:
      type: object
      description: Stand der Anzahlung (nur GET /orders/{id}). `none` = keine vereinbart, `upcoming` = noch nicht fällig, `due` = fällig und nicht gestellt, `draft` = Anzahlungsrechnung im Entwurf, `invoiced` = gestellt (Stornos heben das wieder auf).
      required:
        - status
        - amount_micros
        - due_on
        - invoiced_micros
        - invoices
      properties:
        status:
          type: string
          enum:
            - none
            - upcoming
            - due
            - draft
            - invoiced
        amount_micros:
          type:
            - integer
            - "null"
          format: int64
          description: Vereinbarter Nettobetrag (aus Betrag oder Prozent × Volumen).
        due_on:
          type:
            - string
            - "null"
          format: date
        invoiced_micros:
          type: integer
          format: int64
          description: Netto aller gestellten Anzahlungsrechnungen inklusive Stornos.
        invoices:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              invoice_number:
                type: string
              issue_date:
                type: string
                format: date
              status:
                type: string
              net_micros:
                type: integer
                format: int64
    OrderInput:
      type: object
      description: Schreibbare Felder eines Auftrags (PATCH-Semantik). Geld in Mikro-Euro (µ€), Zeit in Sekunden — immer Ganzzahlen. Textfelder "" = leeren.
      properties:
        customer_id:
          type: string
          format: uuid
          description: Auftraggeber — Pflicht beim Anlegen, nicht auf null setzbar.
        project_ids:
          type:
            - array
            - "null"
          maxItems: 200
          items:
            type: string
            format: uuid
          description: Projekte des Kunden, auf die der Auftrag zählt (ersetzt die bisherige Liste); `[]` oder null = alle Projekte des Kunden. Alle müssen zum Kunden des Auftrags gehören (sonst 422).
        project_id:
          type:
            - string
            - "null"
          format: uuid
          deprecated: true
          description: "Kompatibilität: genau ein Projekt (= `project_ids: [id]`), null = alle Projekte des Kunden. Steht `project_ids` mit im Request, gilt `project_ids`."
        title:
          type: string
          minLength: 1
          maxLength: 200
        order_number:
          type: string
          maxLength: 60
          description: Freie Auftrags-/Bestellnummer, je Workspace eindeutig (aktiver Bestand, sonst 409).
        description:
          type: string
          maxLength: 5000
          description: Leistungsumfang.
        notes:
          type: string
          maxLength: 5000
          description: Interne Notiz.
        buyer_reference:
          type: string
          maxLength: 200
          description: Käuferreferenz bzw. Leitweg-ID für Rechnungen zu diesem Auftrag ("" = leeren). Rechnungen übernehmen sie, solange sie selbst keine tragen; beim Stellen eingefroren.
        order_reference:
          type: string
          maxLength: 200
          description: Bestellnummer des Kunden für Rechnungen zu diesem Auftrag ("" = leeren). Rechnungen übernehmen sie, solange sie selbst keine tragen; beim Stellen eingefroren.
        ordered_on:
          type: string
          format: date
          description: Beauftragungsdatum (YYYY-MM-DD) — Beginn des Zurechnungsfensters.
        ends_on:
          type:
            - string
            - "null"
          format: date
          description: Ende der Laufzeit (einschließlich), nicht vor `ordered_on`; null = offen.
        advance_amount_micros:
          type:
            - integer
            - "null"
          format: int64
          minimum: 1
          maximum: 1000000000000000
          description: Anzahlung als fester Nettobetrag in µ€. Setzen leert `advance_percent_bp`; beide zugleich → 422. null (und `advance_percent_bp` null) = keine Anzahlung.
        advance_percent_bp:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 10000
          description: Anzahlung als Prozentsatz des Umsatzvolumens in Basispunkten (3000 = 30 %); braucht `volume_micros`. Setzen leert `advance_amount_micros`.
        advance_due_on:
          type:
            - string
            - "null"
          format: date
          description: Fälligkeit der Anzahlung; null = sofort mit der Beauftragung. Ohne Anzahlung wird sie geleert.
        volume_micros:
          type:
            - integer
            - "null"
          format: int64
          minimum: 1
          maximum: 1000000000000000
          description: Umsatzvolumen in µ€ (beim Festpreis der vereinbarte Preis).
        time_budget_seconds:
          type:
            - integer
            - "null"
          format: int64
          minimum: 1
          maximum: 1000000000
          description: Zeitkontingent in Sekunden.
        include_non_billable:
          type: boolean
          description: Zehrt nicht abrechenbare Arbeit am Auftrag? Standard false.
        billing_model:
          type: string
          enum:
            - time_and_material
            - fixed_price
          description: Standard `time_and_material`.
        hourly_rate_cents:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000
          description: "Vereinbarter Stundensatz in Euro-Cent — Kalkulationsgrundlage des Volumens bei Time & Material (Zeitkontingent × Satz). Wird nicht gegen `volume_micros` geprüft: ein abweichender Umsatz ist erlaubt, maßgeblich für Fortschritt und Rest bleibt das Volumen."
        allow_overbooking:
          type: boolean
          description: "Darf überbucht werden? Standard false: der Auftrag ist bei 100 % voll, weitere Arbeit läuft in den nächsten passenden Auftrag über oder bleibt „nicht beauftragt“."
        status:
          type: string
          enum:
            - open
            - completed
            - cancelled
        currency:
          type: string
          enum:
            - EUR
    OrderProgress:
      type: object
      description: "Fortschritt eines Auftrags — zugeordnete Leistung gegen vereinbart und abgerechnet. Die Leistung stammt aus der kundenweiten Auftragsbindung: jeder Zeiteintrag liegt in genau einem Auftrag, ein voller Auftrag läuft in den nächsten passenden über."
      required:
        - volumeMicros
        - timeBudgetSeconds
        - performed
        - billedMicros
        - billedSeconds
        - overbooked
        - volumeRatio
        - timeRatio
        - billedRatio
        - remainingVolumeMicros
        - remainingSeconds
        - openToBillMicros
        - unbilledMicros
        - unbilledSeconds
        - status
      properties:
        volumeMicros:
          type:
            - integer
            - "null"
        timeBudgetSeconds:
          type:
            - integer
            - "null"
        performed:
          type: object
          required:
            - revenueMicros
            - seconds
          properties:
            revenueMicros:
              type: integer
            seconds:
              type: integer
        billedMicros:
          type: integer
          description: Summe der Nettobeträge zugeordneter Rechnungen (Stornos negativ).
        billedSeconds:
          type: integer
          description: Zugeordnete Arbeitszeit, die bereits auf einer gestellten Rechnung liegt.
        overbooked:
          type: object
          description: Über das Kontingent hinaus aufgenommen (nur bei `allow_overbooking`).
          required:
            - revenueMicros
            - seconds
          properties:
            revenueMicros:
              type: integer
            seconds:
              type: integer
        volumeRatio:
          type:
            - number
            - "null"
        timeRatio:
          type:
            - number
            - "null"
        billedRatio:
          type:
            - number
            - "null"
        remainingVolumeMicros:
          type:
            - integer
            - "null"
        remainingSeconds:
          type:
            - integer
            - "null"
        openToBillMicros:
          type:
            - integer
            - "null"
        unbilledMicros:
          type: integer
          description: Geleistet, aber noch nicht abgerechnet.
        unbilledSeconds:
          type: integer
          description: Zugeordnete Arbeitszeit, die noch auf keiner gestellten Rechnung liegt.
        status:
          type: string
          enum:
            - ok
            - warning
            - exceeded
          description: Ampel nach der schärferen Grenze (ab 80 % warning, ab 100 % exceeded).
    Statusline:
      type: object
      required:
        - project
        - source
        - today
        - order
      properties:
        project:
          type:
            - object
            - "null"
          required:
            - id
            - name
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        source:
          type:
            - string
            - "null"
          enum:
            - session
            - folder
            - null
          description: "`session` = Zuordnung der KI-Session, `folder` = Ordner-/Repository-Regel."
        today:
          type:
            - object
            - "null"
          required:
            - day
            - human_seconds
            - ai_seconds
            - value_micros
            - cost_micros
          properties:
            day:
              type: string
              format: date
              description: Berliner Kalendertag.
            human_seconds:
              type: integer
              description: Arbeitszeit (Timer).
            ai_seconds:
              type: integer
              description: Agentenzeit (KI-Sessions).
            value_micros:
              type:
                - integer
                - "null"
              description: Leistungswert in µ€; null ohne Rentabilität im Tarif.
            cost_micros:
              type: integer
              description: Kosten in µ€ nach den Kosteneinstellungen des Projekts.
        order:
          type:
            - object
            - "null"
          required:
            - id
            - title
            - unit
            - percent
            - status
          properties:
            id:
              type: string
              format: uuid
            title:
              type: string
            unit:
              type: string
              enum:
                - money
                - time
              description: "`money` = Volumen, `time` = Zeitkontingent (nur ohne Volumen)."
            percent:
              type: integer
              description: Geleistet in Prozent des Kontingents (gerundet, kann über 100 liegen).
            status:
              type: string
              enum:
                - ok
                - warning
                - exceeded
    OrderWindow:
      type: object
      description: Ausgewertetes Zurechnungsfenster (UTC). `endMs` endet spätestens jetzt.
      required:
        - startMs
        - endMs
        - startIso
        - endIso
      properties:
        startMs:
          type: integer
        endMs:
          type: integer
        startIso:
          type: string
          format: date-time
        endIso:
          type:
            - string
            - "null"
          format: date-time
          description: Ende der vereinbarten Laufzeit (exklusiv); null = offen.
    Profile:
      type: object
      required:
        - id
        - email
        - full_name
        - avatar_path
        - avatar_small_path
      properties:
        id:
          type: string
          format: uuid
        email:
          type:
            - string
            - "null"
        full_name:
          type:
            - string
            - "null"
        avatar_path:
          type:
            - string
            - "null"
          description: Speicherpfad des Originals (1024 px) im privaten Bucket; Bytes über GET /profile/avatar?size=full.
        avatar_small_path:
          type:
            - string
            - "null"
          description: Speicherpfad der kleinen Variante (256 px).
    Project:
      type: object
      required:
        - id
        - workspace_id
        - name
        - created_at
        - updated_at
        - deleted_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type:
            - string
            - "null"
        match_keywords:
          type: array
          items:
            type: string
          description: Stichworte für die Zuordnung von KI-Sessions über ihren Titel (siehe ProjectUpdate).
        billing_model:
          $ref: "#/components/schemas/ProjectBillingModel"
        hourly_rate_cents:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000
          description: Stundensatz in Euro-Cent je Stunde (maßgeblich für den Leistungswert). Nur bei Time & Material; bei Festpreis und Pro bono immer null.
        customer_id:
          type:
            - string
            - "null"
          format: uuid
        billable_default:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
        rounding_mode:
          type:
            - string
            - "null"
          enum:
            - none
            - ceil
            - nearest
            - null
        rounding_increment_minutes:
          type:
            - integer
            - "null"
        minimum_entry_minutes:
          type:
            - integer
            - "null"
        include_ai_costs_default:
          type:
            - boolean
            - "null"
        cost_ai_mode:
          type: string
          enum:
            - none
            - api
            - subscription
          description: Wie KI-Nutzung in die Projektkosten eingeht (Standard `api`).
        cost_ai_time_enabled:
          type: boolean
        cost_ai_time_rate_cents:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000
          description: Kostensatz der aktiven KI-Agentenzeit in Cent je Einheit.
        cost_ai_time_rate_unit:
          type: string
          enum:
            - hour
            - day
          description: "`day` = 8 Stunden."
        cost_labor_enabled:
          type: boolean
        cost_labor_rate_cents:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 1000000
          description: Interner Verrechnungssatz der Arbeitszeit in Cent je Einheit.
        cost_labor_rate_unit:
          type: string
          enum:
            - hour
            - day
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - "null"
          format: date-time
          description: Gesetzt = archiviert.
    ProjectBillingModel:
      type: string
      enum:
        - time_and_material
        - fixed_price
        - pro_bono
      description: "Abrechnungsart des Projekts: Time & Material (Arbeitszeit × Stundensatz), Festpreis (vereinbarter Preis aus dem Auftrag, kein Stundensatz) oder Pro bono (nichts abzurechnen, kein Stundensatz). Laufende Aufträge mit Projektbezug übernehmen sie."
    ProjectUpdate:
      description: Mindestens ein Feld; sonst 422 „Keine änderbaren Felder angegeben.“
      allOf:
        - $ref: "#/components/schemas/BillingRuleFields"
        - type: object
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 200
            description:
              type: string
              maxLength: 2000
            match_keywords:
              type:
                - array
                - "null"
              maxItems: 50
              items:
                type: string
                minLength: 2
                maxLength: 100
              description: Ersetzt die Stichworte, über die unzugeordnete KI-Sessions anhand ihres Titels diesem Projekt zugeordnet werden (null oder [] leert). Neue Stichworte ordnen offene Sessions sofort rückwirkend zu.
            billing_model:
              $ref: "#/components/schemas/ProjectBillingModel"
            hourly_rate_cents:
              type:
                - integer
                - "null"
              minimum: 0
              maximum: 1000000
              description: null = leeren. Ein Satz für ein Festpreis- oder Pro-bono-Projekt → 422.
            customer_id:
              type:
                - string
                - "null"
              format: uuid
              description: null = Kundenzuordnung lösen; fremde ID → 422.
            archived:
              type: boolean
              description: true = archivieren, false = wiederherstellen.
            cost_ai_mode:
              type: string
              enum:
                - none
                - api
                - subscription
            cost_ai_time_enabled:
              type: boolean
            cost_ai_time_rate_cents:
              type:
                - integer
                - "null"
              minimum: 0
              maximum: 1000000
            cost_ai_time_rate_unit:
              type: string
              enum:
                - hour
                - day
            cost_labor_enabled:
              type: boolean
            cost_labor_rate_cents:
              type:
                - integer
                - "null"
              minimum: 0
              maximum: 1000000
            cost_labor_rate_unit:
              type: string
              enum:
                - hour
                - day
    ProjectCostBreakdown:
      type: object
      description: Kosten eines Projekts im Zeitraum je Kostenart (µ€).
      properties:
        ai_mode:
          type: string
          enum:
            - none
            - api
            - subscription
        ai_micros:
          type: integer
          description: KI-Kosten nach Modus.
        api_cost_micros:
          type: integer
          description: KI-Kosten zu API-Preisen (Vergleichswert).
        subscription_share_micros:
          type: integer
          description: Anteil an den KI-Abos (Vergleichswert).
        ai_time_micros:
          type: integer
        labor_micros:
          type: integer
        expenses_micros:
          type: integer
        total_micros:
          type: integer
    ProjectCostReport:
      type: object
      properties:
        window:
          type: object
          properties:
            fromDay:
              type:
                - string
                - "null"
              format: date
            toDay:
              type: string
              format: date
        settings:
          type: object
          additionalProperties: true
          description: Die `cost_*`-Felder des Projekts.
        costs:
          $ref: "#/components/schemas/ProjectCostBreakdown"
        subscriptions:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              provider:
                type: string
                enum:
                  - anthropic
                  - chatgpt
                  - google
                  - xai
              allocated_micros:
                type: integer
                description: Im Zeitraum auf Projekte verteilter Betrag.
              project_share_micros:
                type: integer
                description: Anteil dieses Projekts.
    AiSubscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        provider:
          type: string
          enum:
            - anthropic
            - chatgpt
            - google
            - xai
          description: "Anbieter: anthropic = Claude, chatgpt = ChatGPT (GPT, Codex), google = Gemini, xai = Grok. Erkannt am Modell der Session."
        monthly_amount_cents:
          type: integer
        vat_paid:
          type: boolean
        vat_rate_bp:
          type: integer
        valid_from:
          type: string
          format: date
          description: Monatserster.
        valid_to:
          type:
            - string
            - "null"
          format: date
          description: Monatserster
          inklusive; null = unbefristet.: null
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AiSubscriptionInput:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        provider:
          type: string
          enum:
            - anthropic
            - chatgpt
            - google
            - xai
        monthly_amount_cents:
          type: integer
          minimum: 0
          maximum: 100000000
          description: Monatsbetrag netto in Cent.
        vat_paid:
          type: boolean
        vat_rate_bp:
          type: integer
          minimum: 0
          maximum: 10000
        valid_from:
          type: string
          format: date
          description: Wird auf den Monatsersten gelegt.
        valid_to:
          type:
            - string
            - "null"
          format: date
    ProjectExpense:
      type: object
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        expense_date:
          type: string
          format: date
        title:
          type: string
        description:
          type:
            - string
            - "null"
        supplier:
          type:
            - string
            - "null"
        amount_net_cents:
          type: integer
          description: Nettobetrag — geht in die Rentabilität.
        vat_paid:
          type: boolean
          description: Mit Mehrwertsteuer bezahlt (Vorsteuer).
        vat_rate_bp:
          type: integer
        vat_amount_cents:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        billed_on:
          type:
            - object
            - "null"
          description: Rechnung, auf der die Einzelkosten weiterbelastet sind.
          properties:
            invoice_id:
              type: string
              format: uuid
            invoice_number:
              type:
                - string
                - "null"
            status:
              type:
                - string
                - "null"
    ProjectExpenseInput:
      type: object
      properties:
        expense_date:
          type: string
          format: date
        title:
          type: string
          minLength: 1
          maxLength: 300
        description:
          type: string
          maxLength: 2000
        supplier:
          type: string
          maxLength: 200
        amount_net_cents:
          type: integer
          minimum: 0
          maximum: 100000000000
        vat_paid:
          type: boolean
        vat_rate_bp:
          type: integer
          minimum: 0
          maximum: 10000
        vat_amount_cents:
          type: integer
          minimum: 0
    RevenueFigures:
      type: object
      description: |
        Der gemeinsame Kennzahlensatz — identisch je Kunde, je Projekt und in der Gesamtsumme. Geld in Mikro-Euro (µ€), Mengen als Sekunden bzw. Netto-Token.
      required:
        - performance_value_micros
        - invoiced_micros
        - open_micros
        - paid_micros
        - unbilled_micros
        - ai_cost_micros
        - ai_unpriced_sessions
        - ai_time_cost_micros
        - labor_cost_micros
        - expense_cost_micros
        - cost_micros
        - contribution_micros
        - seconds
        - tokens
      properties:
        performance_value_micros:
          type: integer
          description: Leistungswert — kalkulatorisch, Stundensatz × abrechenbare Zeit.
        invoiced_micros:
          type: integer
          description: Fakturiert — Allocations aus Rechnungen mit Status `final`, `sent` oder `paid`.
        open_micros:
          type: integer
          description: Offene Posten — fakturiert, aber noch nicht bezahlt.
        paid_micros:
          type: integer
          description: Bezahlt — `paid` voll, `final`/`sent` anteilig nach erfassten Zahlungen.
        unbilled_micros:
          type: integer
          description: |
            Nicht fakturiert — Leistungswert − Fakturiert. DARF NEGATIV SEIN; das heißt dann „mehr fakturiert als geleistet“ und ist in der Ansicht auch so zu beschriften.
        ai_cost_micros:
          type: integer
          description: |
            KI-Kosten nach dem Modus je Projekt (`cost_ai_mode`): versionierte Modellpreise (USD-Listenpreise mit dem EZB-Kurs vom Tag des Session-Starts in Euro umgerechnet), Anteil an den KI-Abos oder 0.
        ai_unpriced_sessions:
          type: integer
          minimum: 0
          description: |
            Sessions mit Token, aber ohne greifenden Modellpreis, die in `ai_cost_micros` mit 0 € stecken (nur Modus `api`). Größer 0 heißt: `ai_cost_micros` und `cost_micros` sind eine Untergrenze, `contribution_micros` eine Obergrenze (Anzeige „mind.“/„höchst.“).
        ai_time_cost_micros:
          type: integer
          description: KI-Agentenzeit × Kostensatz (Projekte mit eingeschalteter Agentenzeit).
        labor_cost_micros:
          type: integer
          description: Arbeitszeit × interner Verrechnungssatz (Projekte mit eingeschalteter Arbeitszeit).
        expense_cost_micros:
          type: integer
          description: Einzelkosten netto mit Datum im Zeitraum.
        cost_micros:
          type: integer
          description: Kosten gesamt — Summe der vier Kostenarten.
        contribution_micros:
          type: integer
          description: Deckungsbeitrag — Fakturiert − Kosten.
        seconds:
          type: integer
          description: Fakturierte Arbeitszeit (nur aus zählenden Rechnungen).
        tokens:
          type: integer
          description: Fakturierte Netto-Token (nur aus zählenden Rechnungen).
    RevenueSummary:
      type: object
      description: Ergebnis von `GET /revenue`.
      required:
        - perCustomer
        - perProject
        - totals
        - months
      properties:
        perCustomer:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/RevenueFigures"
              - type: object
                required:
                  - key
                  - customer_id
                  - customer_name
                properties:
                  key:
                    type: string
                    description: Stabiler Gruppenschlüssel für Tabellen und Listen.
                  customer_id:
                    type:
                      - string
                      - "null"
                    format: uuid
                  customer_name:
                    type:
                      - string
                      - "null"
                    description: null = „Ohne Kunde“.
        perProject:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/RevenueFigures"
              - type: object
                required:
                  - key
                  - project_id
                  - project_name
                  - customer_id
                properties:
                  key:
                    type: string
                  project_id:
                    type:
                      - string
                      - "null"
                    format: uuid
                  project_name:
                    type:
                      - string
                      - "null"
                    description: null = „Ohne Projekt“.
                  customer_id:
                    type:
                      - string
                      - "null"
                    format: uuid
        totals:
          $ref: "#/components/schemas/RevenueFigures"
        months:
          type: array
          description: Monatsverlauf über das angefragte Fenster — auch Monate ohne Fakturierung sind enthalten, damit die Achse keine Lücken hat.
          items:
            type: object
            required:
              - period_month
              - invoiced_micros
              - open_micros
              - paid_micros
              - performance_value_micros
            properties:
              period_month:
                type: string
                format: date
                description: Monatserster des Rechnungsdatums (YYYY-MM-01).
              invoiced_micros:
                type: integer
              open_micros:
                type: integer
              paid_micros:
                type: integer
              performance_value_micros:
                type: integer
                description: Leistungswert des Monats (aus der Aggregation).
    ShareCard:
      type: object
      description: Eigene Share-Karte (Owner-Sicht inkl. Token — der Token gehört dem Besitzer).
      required:
        - id
        - user_id
        - token
        - display_name
        - avatar_path
        - enabled
        - use_profile_avatar
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        token:
          type: string
          description: Öffentliches Token für /s/{token} (22 Zeichen base62).
        display_name:
          type: string
          minLength: 1
          maxLength: 80
        avatar_path:
          type:
            - string
            - "null"
          description: |
            Interner Storage-Pfad im privaten Bucket share-avatars (null ohne Bild). Nur in der Owner-Sicht; öffentlich wird der Pfad nie exponiert (Auslieferung über /s/{token}/avatar).
        enabled:
          type: boolean
          description: Öffentlich sichtbar (Opt-in; Default false).
        use_profile_avatar:
          type: boolean
          description: Zeigt das Profilfoto statt eines eigenen Avatars.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ShareCardStats:
      type: object
      description: Aggregierte Token-Kennzahlen des Nutzers (Live-Vorschau, unabhängig von enabled).
      required:
        - last7d_tokens
        - last7d_changed_lines
        - top_models
        - generated_at
      properties:
        last7d_tokens:
          type: integer
          description: Netto-Token der letzten 7 Tage (frischer Input + Cache-Erzeugung + Output, ohne Cache-Reads).
        last7d_changed_lines:
          type: integer
          description: Geänderte Codezeilen der letzten 7 Tage (aus Commits).
        top_models:
          type: array
          maxItems: 3
          items:
            type: object
            required:
              - model
              - tokens
            properties:
              model:
                type: string
              tokens:
                type: integer
                description: Netto-Token dieses Modells (gleiche Formel wie last7d_tokens).
        generated_at:
          type: string
          format: date-time
    SkippedBilled:
      type: object
      description: |
        Was übersprungen wurde, weil es bereits auf einer ANDEREN Rechnung liegt (Sperrvermerke gegen Doppelfakturierung). Die Mengen sind die übersprungenen, nicht die abgerechneten — ohne diese Angabe suchte der Nutzer den Fehler für eine unerwartet magere Rechnung in seiner Zeiterfassung.
      properties:
        timers:
          type: integer
        sessions:
          type: integer
        seconds:
          type: integer
        tokens:
          type: integer
        invoice_numbers:
          type: array
          description: Nummern der Rechnungen, auf denen die übersprungenen Leistungen liegen.
          items:
            type: string
    Statement:
      type: object
      description: |
        Ein Nachweis, wie ihn Liste und Detail liefern — stets OHNE `snapshot` (das Dokument liefert `GET /statements/{id}/document`). Ein Entwurf liest Absender, Texte und Layout live aus der Rechnungsvorlage; die `sender_*`-Felder, `issue_date` und `layout` sind erst ab dem Festschreiben der eingefrorene Stand.
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        customer_id:
          type: string
          format: uuid
        statement_number:
          type: string
          description: |
            Bis zum Festschreiben ein Platzhalter `ENTWURF-…` (oder eine eigene Nummer), danach die vergebene Nummer aus dem Nachweis-Nummernkreis.
        title:
          type: string
        kind:
          type: string
          enum:
            - proof
            - progress
        period_from:
          type: string
          format: date
        period_to:
          type: string
          format: date
        source_invoice_id:
          type:
            - string
            - "null"
          format: uuid
          description: Bezugsrechnung (beim Erzeugen aus einer Rechnung gesetzt). Folgt die Auswahl ihr (`content_config.selection.source = invoice`), zeigt der Nachweis genau deren Leistungen. Muss zum Kunden des Nachweises gehören.
        order_id:
          type:
            - string
            - "null"
          format: uuid
          description: Auftrag, den der Nachweis belegt (null = ohne). Muss zum Kunden gehören.
        project_ids:
          type:
            - array
            - "null"
          items:
            type: string
            format: uuid
        scope:
          type: object
          deprecated: true
          description: |
            Alter Inhaltsumfang (vor den Bausteinen). Nur noch Rückfall für ein leeres `content_config`; nicht mehr schreibbar.
        content_config:
          type: object
          description: |
            Gespeicherte Bausteinkonfiguration, roh. Aufgelöst liefert sie `GET /statements/{id}` als `config` (`StatementContentConfig`); geschrieben wird sie über `PUT /statements/{id}/content`.
        wizard_step:
          type:
            - string
            - "null"
          enum:
            - rahmen
            - inhalte
            - pruefen
            - null
          description: Zuletzt geöffneter Schritt des Assistenten.
        intro_text:
          type:
            - string
            - "null"
          description: Eigene Einleitung; null = Einleitung der Vorlage (`statementIntroText`).
        closing_text:
          type:
            - string
            - "null"
          description: Eigener Schlusstext; null = Schlusstext der Vorlage.
        recipient_name:
          type:
            - string
            - "null"
        recipient_address:
          type:
            - string
            - "null"
        sender_name:
          type:
            - string
            - "null"
        sender_address:
          type:
            - string
            - "null"
        sender_email:
          type:
            - string
            - "null"
        sender_phone:
          type:
            - string
            - "null"
        sender_website:
          type:
            - string
            - "null"
        sender_vat_id:
          type:
            - string
            - "null"
        sender_tax_number:
          type:
            - string
            - "null"
        sender_register:
          type:
            - string
            - "null"
        sender_register_court:
          type:
            - string
            - "null"
        sender_managing_directors:
          type:
            - string
            - "null"
        signer_name:
          type:
            - string
            - "null"
        greeting_text:
          type:
            - string
            - "null"
        layout:
          type: object
          description: |
            Entwurf: leer (das Layout kommt live aus der Vorlage). Festgeschrieben: Schnappschuss mit den Schlüsseln von `InvoiceLayoutConfig`, bei einem eigenen Layout zusätzlich `custom_source` (Quelltext).
        status:
          type: string
          enum:
            - draft
            - final
          description: |
            Ab `final` ist der Nachweis inhaltlich gesperrt und rendert aus seinem eingefrorenen Snapshot.
        finalized_at:
          type:
            - string
            - "null"
          format: date-time
        issue_date:
          type:
            - string
            - "null"
          format: date
          description: Ausstellungsdatum (beim Festschreiben gesetzt; im Entwurf null = heute).
        pdf_path:
          type:
            - string
            - "null"
          description: Abgelegtes PDF eines festgeschriebenen Nachweises (intern; Download über `/export`).
        client_id:
          type:
            - string
            - "null"
          format: uuid
        client_updated_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - "null"
          format: date-time
          description: Gesetzt = archiviert.
    StatementInput:
      type: object
      description: |
        Schreibbare Kopffelder eines Nachweises. Beim Anlegen ist nur `customer_id` Pflicht, alles Übrige wird vorbefüllt; bei `PATCH` ist alles optional (PATCH-Semantik). Leerstring in einem Freitextfeld bedeutet „löschen“ (→ null). Bausteine, Auswahl und Darstellung laufen über `PUT /statements/{id}/content`.
      properties:
        customer_id:
          type: string
          format: uuid
          description: |
            PFLICHT und nie leerbar. Umhängen auf einen anderen Kunden bleibt erlaubt; die Projektauswahl muss danach zu ihm passen.
        statement_number:
          type:
            - string
            - "null"
          maxLength: 60
          description: |
            Eigene Nummer, je Workspace eindeutig (409). `ENTWURF-…` ist reserviert (422). null/"" = Platzhalter — dann vergibt das Festschreiben die Nummer aus dem Nachweis-Nummernkreis der Vorlage (`statementNumber*`).
        title:
          type: string
          maxLength: 120
          description: Vorbelegt mit dem Label der Art („Leistungsnachweis“ / „Fortschrittsbericht“).
        kind:
          type: string
          enum:
            - proof
            - progress
          default: proof
          description: |
            `proof` = Leistungsnachweis (Standardauswahl: abrechenbare Leistungen), `progress` = Fortschrittsbericht (Standardauswahl: alle Leistungen, nicht abrechenbare gedimmt).
        period_from:
          type: string
          format: date
          description: Beim Anlegen optional (dann Vormonat) — aber nur zusammen mit `period_to`.
        period_to:
          type: string
          format: date
          description: Muss `>= period_from` sein (sonst 422).
        source_invoice_id:
          type:
            - string
            - "null"
          format: uuid
          description: Bezugsrechnung. Muss zum Kunden des Nachweises gehören.
        order_id:
          type:
            - string
            - "null"
          format: uuid
          description: Auftrag, den der Nachweis belegt (null = ohne). Muss zum Kunden gehören.
        project_ids:
          type:
            - array
            - "null"
          maxItems: 200
          items:
            type: string
            format: uuid
          description: |
            Einschränkung auf einzelne Projekte. Leer/null = alle Projekte des Kunden — ein leeres Array wird als „keine Einschränkung“ gelesen, sonst erzeugte eine versehentlich geleerte Auswahl still einen leeren Nachweis. Es sind ausschließlich Projekte DIESES Kunden erlaubt (422).
        wizard_step:
          type:
            - string
            - "null"
          enum:
            - rahmen
            - inhalte
            - pruefen
            - null
          description: Zuletzt geöffneter Schritt des Assistenten (auch am festgeschriebenen Nachweis).
        intro_text:
          type:
            - string
            - "null"
          maxLength: 5000
          description: Eigene Einleitung; null = die der Vorlage. Platzhalter {zeitraum} {nummer} {kunde}.
        closing_text:
          type:
            - string
            - "null"
          maxLength: 5000
        recipient_name:
          type:
            - string
            - "null"
          maxLength: 200
          description: Empfänger, beim Anlegen aus dem Kunden vorbefüllt.
        recipient_address:
          type:
            - string
            - "null"
          maxLength: 1000
        sender_name:
          type:
            - string
            - "null"
          maxLength: 200
          description: Wird beim Festschreiben aus der Vorlage überschrieben, sofern sie einen Absender hat.
        sender_address:
          type:
            - string
            - "null"
          maxLength: 1000
    StatementManualRow:
      type: object
      description: Eigene Leistung (Baustein `manual`).
      required:
        - id
        - day
        - title
        - description
        - quantityMilli
        - unit
        - amountMicros
      properties:
        id:
          type: string
          description: Stabile Kennung (uuid), damit Zeilen beim Bearbeiten nicht springen.
        day:
          type:
            - string
            - "null"
          format: date
        title:
          type: string
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 1000
        quantityMilli:
          type:
            - integer
            - "null"
          description: Menge in Tausendsteln (1500 = 1,5); null = ohne Menge.
        unit:
          type: string
          maxLength: 20
        amountMicros:
          type:
            - integer
            - "null"
          description: Betrag in µ€; null = ohne Betrag (der Normalfall im Nachweis).
    StatementContentConfig:
      type: object
      description: |
        Bausteine, Auswahl und Darstellung eines Nachweises. AUSWAHL (welche Leistungen) und DARSTELLUNG (wie gruppiert, wie granular) sind getrennt. Robust aufgelöst: unbekannte Schlüssel fallen weg, ungültige Werte auf den Standard. Beim Schreiben IMMER das ganze Objekt senden.
      required:
        - blocks
        - structure
        - subtotals
        - signature
        - selection
        - time
        - ai
        - expenses
        - commits
        - summary
        - manual
        - text
        - progress
      properties:
        blocks:
          type: array
          description: Aktive Bausteine in Dokumentreihenfolge (doppelte fallen weg).
          items:
            type: string
            enum:
              - summary
              - progress
              - time
              - ai
              - expenses
              - commits
              - manual
              - text
        structure:
          type: string
          enum:
            - blocks
            - project
          description: |
            `blocks` = je Baustein ein Abschnitt; `project` = je Projekt ein Kapitel mit den Bausteinen darin (Kennzahlen, eigene Leistungen und Hinweistext bleiben außerhalb).
        subtotals:
          type: boolean
          description: Zwischensummen je Gruppe.
        signature:
          type:
            - boolean
            - "null"
          description: Unterschriftsfeld „Leistung bestätigt“; null = Einstellung der Vorlage (`statementSignature`).
        headFields:
          type: object
          description: |
            Kopfdaten nur für diesen Nachweis ein- oder ausblenden; je Schlüssel null = Einstellung der Vorlage (`statementHeadFields`). Fehlende Schlüssel gelten als null.
          properties:
            period:
              type:
                - boolean
                - "null"
            customerNumber:
              type:
                - boolean
                - "null"
            projects:
              type:
                - boolean
                - "null"
            order:
              type:
                - boolean
                - "null"
            invoice:
              type:
                - boolean
                - "null"
        selection:
          type: object
          required:
            - source
            - excludedTimerIds
            - includedTimerIds
            - excludedSessionIds
            - includedSessionIds
            - includedExpenseIds
          description: |
            Wie bei der Rechnung werden nur ABWEICHUNGEN vom Standard gespeichert (Art `proof`: abrechenbare Leistungen, `progress`: alle). Projektkosten werden ausdrücklich gewählt.
          properties:
            source:
              type: string
              enum:
                - own
                - invoice
              description: |
                `own` = eigene Auswahl; `invoice` = genau die Leistungen der Bezugsrechnung (live, solange beide Entwürfe sind). `POST /statements/{id}/detach` macht daraus `own`.
            excludedTimerIds:
              type: array
              items:
                type: string
            includedTimerIds:
              type: array
              items:
                type: string
            excludedSessionIds:
              type: array
              items:
                type: string
            includedSessionIds:
              type: array
              items:
                type: string
            includedExpenseIds:
              type: array
              items:
                type: string
        time:
          type: object
          description: Baustein Arbeitszeit.
          properties:
            groupBy:
              type: string
              enum:
                - none
                - project
                - month
                - week
                - day
              description: Gruppenüberschriften innerhalb des Abschnitts.
            rows:
              type: string
              enum:
                - entry
                - dayDescription
                - day
                - description
                - project
                - sum
              description: Detailgrad — was eine Zeile ist (`sum` = nur die Summe).
            showTimes:
              type: boolean
              description: Uhrzeit von–bis (nur bei `rows = entry`).
            showDescription:
              type: boolean
            showProject:
              type: boolean
              description: Entfällt automatisch, wenn nach Projekt gruppiert wird.
            showRate:
              type: boolean
            showValue:
              type: boolean
              description: Leistungswert (Satz × Zeit).
            label:
              type: string
              maxLength: 120
        ai:
          type: object
          description: Baustein KI-Nutzung.
          properties:
            groupBy:
              type: string
              enum:
                - none
                - project
                - month
                - week
                - day
                - model
            rows:
              type: string
              enum:
                - session
                - day
                - model
                - project
                - sum
            showModel:
              type: boolean
            showTool:
              type: boolean
            showTokens:
              type: boolean
            showRuntime:
              type: boolean
            showCost:
              type: boolean
            label:
              type: string
              maxLength: 120
        expenses:
          type: object
          properties:
            showAmounts:
              type: boolean
            label:
              type: string
              maxLength: 120
        commits:
          type: object
          properties:
            grouping:
              type: string
              enum:
                - list
                - week
                - project
            label:
              type: string
              maxLength: 120
        summary:
          type: object
          properties:
            showHours:
              type: boolean
            showAi:
              type: boolean
            showValue:
              type: boolean
            label:
              type: string
              maxLength: 120
        manual:
          type: object
          properties:
            label:
              type: string
              maxLength: 120
            rows:
              type: array
              maxItems: 200
              items:
                $ref: "#/components/schemas/StatementManualRow"
        text:
          type: object
          properties:
            body:
              type: string
              maxLength: 5000
              description: Hinweistext (Baustein `text`).
        progress:
          type: object
          description: "Baustein „Fortschritt“: reine Kopie der Fortschrittspunkte (füllen über `POST /statements/{id}/progress`), im Nachweis frei bearbeitbar."
          properties:
            label:
              type: string
              maxLength: 120
            source:
              type: string
              enum:
                - weeks
                - report
            showEffort:
              type: boolean
              description: Aufwand je Punkt (Standard aus).
            showDates:
              type: boolean
            showTech:
              type: boolean
            order:
              type: string
              enum:
                - importance
                - chronological
            fetchedAt:
              type:
                - string
                - "null"
              format: date-time
            fetchedSource:
              type:
                - string
                - "null"
              enum:
                - weeks
                - report
                - null
            projects:
              type: array
              maxItems: 50
              items:
                type: object
                required:
                  - projectId
                  - projectName
                  - items
                properties:
                  projectId:
                    type: string
                  projectName:
                    type: string
                  summary:
                    type:
                      - string
                      - "null"
                    maxLength: 1200
                  items:
                    type: array
                    maxItems: 300
                    items:
                      type: object
                      required:
                        - id
                        - title
                      properties:
                        id:
                          type: string
                        title:
                          type: string
                          maxLength: 200
                        body:
                          type: string
                          maxLength: 2000
                        kind:
                          type: string
                          enum:
                            - new
                            - improved
                            - fixed
                            - tech
                            - alignment
                        status:
                          type: string
                          enum:
                            - done
                            - in_progress
                        from:
                          type:
                            - string
                            - "null"
                          format: date
                        to:
                          type:
                            - string
                            - "null"
                          format: date
                        rank:
                          type: integer
                        humanSeconds:
                          type: integer
                        aiSeconds:
                          type: integer
    StatementCandidates:
      type: object
      description: |
        Auswahlgrundlage eines Nachweises. Tage als ISO-Datum nach Europe/Berlin, Zeitpunkte ISO. `billedOn` = Nummer der Rechnung, auf der die Leistung steht — nur informativ.
      required:
        - period
        - kind
        - customer
        - projects
        - timers
        - sessions
        - expenses
        - commits
        - invoice
        - order
        - lastStatementTo
      properties:
        period:
          type: object
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
        kind:
          type: string
          enum:
            - proof
            - progress
        customer:
          type: object
          required:
            - id
            - name
            - customerNumber
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            customerNumber:
              type:
                - string
                - "null"
        projects:
          type: array
          description: Alle nicht archivierten Projekte des Kunden (+ bereits gewählte archivierte).
          items:
            type: object
            required:
              - id
              - name
              - selected
              - seconds
              - sessions
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              selected:
                type: boolean
              seconds:
                type: integer
              sessions:
                type: integer
        timers:
          type: array
          items:
            type: object
            required:
              - id
              - day
              - startedAt
              - endedAt
              - projectId
              - projectName
              - description
              - billableSeconds
              - activeSeconds
              - billable
              - hourlyRateCents
              - billedOn
            properties:
              id:
                type: string
                format: uuid
              day:
                type: string
                format: date
              startedAt:
                type:
                  - string
                  - "null"
                format: date-time
              endedAt:
                type:
                  - string
                  - "null"
                format: date-time
              projectId:
                type:
                  - string
                  - "null"
                format: uuid
              projectName:
                type: string
              description:
                type:
                  - string
                  - "null"
              billableSeconds:
                type: integer
                description: Gerundet; 0, wenn nicht abrechenbar.
              activeSeconds:
                type: integer
              billable:
                type: boolean
              hourlyRateCents:
                type:
                  - integer
                  - "null"
                description: Wirksamer Stundensatz (Projekt → Kunde) in Cent/h.
              billedOn:
                type:
                  - string
                  - "null"
              inFinalStatements:
                type: array
                items:
                  type: string
                description: Nummern festgeschriebener Leistungsnachweise, die den Eintrag belegen — wählbar, aber nicht mehr änderbar oder löschbar.
        sessions:
          type: array
          items:
            type: object
            required:
              - id
              - day
              - startedAt
              - projectId
              - projectName
              - model
              - tool
              - netTokens
              - costMicros
              - activeSeconds
              - billable
              - billedOn
            properties:
              id:
                type: string
                format: uuid
              day:
                type: string
                format: date
              startedAt:
                type:
                  - string
                  - "null"
                format: date-time
              projectId:
                type:
                  - string
                  - "null"
                format: uuid
              projectName:
                type: string
              model:
                type:
                  - string
                  - "null"
              tool:
                type:
                  - string
                  - "null"
              netTokens:
                type: integer
              costMicros:
                type:
                  - integer
                  - "null"
                description: null = ungepreist.
              activeSeconds:
                type: integer
              billable:
                type: boolean
              billedOn:
                type:
                  - string
                  - "null"
        expenses:
          type: array
          items:
            type: object
            required:
              - id
              - projectId
              - projectName
              - date
              - title
              - description
              - supplier
              - netCents
              - billedOn
            properties:
              id:
                type: string
                format: uuid
              projectId:
                type: string
                format: uuid
              projectName:
                type: string
              date:
                type: string
                format: date
              title:
                type: string
              description:
                type:
                  - string
                  - "null"
              supplier:
                type:
                  - string
                  - "null"
              netCents:
                type: integer
              billedOn:
                type:
                  - string
                  - "null"
        commits:
          type: array
          description: Nur geladen, wenn der Baustein `commits` aktiv ist oder `?commits=true`; sonst leer.
          items:
            type: object
            required:
              - day
              - projectName
              - repo
              - sha
              - message
              - author
            properties:
              day:
                type: string
                format: date
              projectName:
                type: string
              repo:
                type:
                  - string
                  - "null"
              sha:
                type:
                  - string
                  - "null"
              message:
                type: string
              author:
                type:
                  - string
                  - "null"
        invoice:
          description: Bezugsrechnung (`source_invoice_id`) samt ihrer Leistungs-IDs, sonst null.
          oneOf:
            - type: object
              required:
                - id
                - number
                - status
                - entityIds
              properties:
                id:
                  type: string
                  format: uuid
                number:
                  type: string
                  description: „Entwurf“ bei Platzhalternummer.
                status:
                  type: string
                entityIds:
                  type: object
                  required:
                    - timers
                    - sessions
                    - expenses
                  properties:
                    timers:
                      type: array
                      items:
                        type: string
                    sessions:
                      type: array
                      items:
                        type: string
                    expenses:
                      type: array
                      items:
                        type: string
            - type: "null"
        order:
          oneOf:
            - type: object
              required:
                - id
                - title
              properties:
                id:
                  type: string
                  format: uuid
                title:
                  type: string
            - type: "null"
        lastStatementTo:
          type:
            - string
            - "null"
          format: date
          description: Ende des letzten festgeschriebenen Nachweises dieses Kunden (Vorschlag „Seit letztem Nachweis“).
        progressEnabled:
          type: boolean
          description: Workspace-Schalter Projektfortschritt — aus = Baustein `progress` gesperrt.
    StatementCell:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          description: Anzeige (de-DE), NICHT HTML-maskiert.
        value:
          type:
            - string
            - number
            - "null"
          description: Rohwert für Excel.
    StatementTable:
      type: object
      required:
        - columns
        - groups
        - total
        - note
      properties:
        columns:
          type: array
          items:
            type: object
            required:
              - key
              - label
              - align
            properties:
              key:
                type: string
              label:
                type: string
              align:
                type: string
                enum:
                  - left
                  - right
              width:
                type: string
                enum:
                  - narrow
                  - normal
                  - wide
        groups:
          type: array
          items:
            type: object
            required:
              - title
              - rows
              - subtotal
            properties:
              title:
                type:
                  - string
                  - "null"
                description: null = ohne Gruppenüberschrift.
              rows:
                type: array
                items:
                  type: object
                  required:
                    - cells
                  properties:
                    cells:
                      type: array
                      items:
                        $ref: "#/components/schemas/StatementCell"
                    muted:
                      type: boolean
                      description: Nicht abrechenbare Leistung im Fortschrittsbericht.
                    detail:
                      type:
                        - string
                        - "null"
                      description: Zweite Zeile unter der ersten Spalte.
              subtotal:
                type:
                  - array
                  - "null"
                items:
                  $ref: "#/components/schemas/StatementCell"
        total:
          type:
            - array
            - "null"
          items:
            $ref: "#/components/schemas/StatementCell"
        note:
          type:
            - string
            - "null"
    StatementBodySection:
      type: object
      required:
        - key
        - block
        - title
        - kind
      properties:
        key:
          type: string
          description: Stabil, z. B. `time`, `project:<id>`, `project:<id>:time`.
        block:
          type: string
          enum:
            - summary
            - progress
            - time
            - ai
            - expenses
            - commits
            - manual
            - text
            - project
        title:
          type: string
        kind:
          type: string
          enum:
            - table
            - text
            - kpis
            - chapter
        table:
          $ref: "#/components/schemas/StatementTable"
        paragraphs:
          type: array
          description: Bei `text` die Absätze, bei `table` Absätze über der Tabelle (Zusammenfassung des Fortschritts).
          items:
            type: string
        kpis:
          type: array
          items:
            type: object
            required:
              - label
              - value
            properties:
              label:
                type: string
              value:
                type: string
              raw:
                type:
                  - number
                  - "null"
        children:
          type: array
          description: Nur bei `kind = chapter` (höchstens eine Ebene tief).
          items:
            $ref: "#/components/schemas/StatementBodySection"
    StatementHeadFields:
      type: object
      description: |
        Kopfdaten des Leistungsnachweises ein (true) oder aus (false). Nummer und Datum stehen immer da. In der Vorlage der Standard (fehlender Schlüssel = an), im Inhalt eines Nachweises die aufgelöste Entscheidung (Nachweis vor Vorlage).
      properties:
        period:
          type: boolean
          description: Zeitraum (der Platzhalter {zeitraum} in den Texten wird weiter ersetzt).
        customerNumber:
          type: boolean
          description: Kundennummer.
        projects:
          type: boolean
          description: Projekte.
        order:
          type: boolean
          description: Auftrag.
        invoice:
          type: boolean
          description: Zur Rechnung (Nummer der Bezugsrechnung).
    StatementBody:
      type: object
      description: |
        Fertig gegliederter Inhalt eines Nachweises (Typ `StatementBody`) — dieselbe Struktur verwenden Vorschau, Dokument, Snapshot und Excel/CSV.
      required:
        - sections
        - totals
        - signature
        - headFields
        - warnings
      properties:
        sections:
          type: array
          items:
            $ref: "#/components/schemas/StatementBodySection"
        totals:
          type: object
          required:
            - seconds
            - billableSeconds
            - entries
            - aiSeconds
            - sessions
            - tokens
            - costMicros
            - valueMicros
            - expensesCents
            - commits
          properties:
            seconds:
              type: integer
            billableSeconds:
              type: integer
            entries:
              type: integer
            aiSeconds:
              type: integer
            sessions:
              type: integer
            tokens:
              type: integer
            costMicros:
              type:
                - integer
                - "null"
            valueMicros:
              type:
                - integer
                - "null"
              description: Leistungswert der Arbeitszeit (null = kein Satz).
            expensesCents:
              type: integer
            commits:
              type: integer
        signature:
          type: boolean
        headFields:
          $ref: "#/components/schemas/StatementHeadFields"
        warnings:
          type: array
          description: Hinweise für die Oberfläche (nicht im Dokument), z. B. leere Auswahl, ungepreiste Modelle.
          items:
            type: string
    StatementContentResult:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          required:
            - config
            - body
          properties:
            config:
              $ref: "#/components/schemas/StatementContentConfig"
            body:
              $ref: "#/components/schemas/StatementBody"
    SupportRequest:
      type: object
      required:
        - id
        - workspace_id
        - type
        - subject
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - support
            - feedback
        subject:
          type: string
        status:
          type: string
          enum:
            - open
            - answered
            - closed
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SupportMessage:
      type: object
      required:
        - id
        - request_id
        - author_role
        - body
        - created_at
      properties:
        id:
          type: string
          format: uuid
        request_id:
          type: string
          format: uuid
        author_role:
          type: string
          enum:
            - customer
            - admin
          description: "`customer` = eigene Nachricht, `admin` = Antwort des Supports."
        body:
          type: string
        created_at:
          type: string
          format: date-time
    Timer:
      type: object
      required:
        - id
        - workspace_id
        - project_id
        - description
        - status
        - started_at
        - ended_at
        - accumulated_seconds
        - last_resumed_at
        - billability
        - created_at
        - updated_at
        - active_seconds
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        project_id:
          type:
            - string
            - "null"
          format: uuid
        description:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - running
            - paused
            - stopped
        started_at:
          type: string
          format: date-time
        ended_at:
          type:
            - string
            - "null"
          format: date-time
        accumulated_seconds:
          type: integer
          description: Bis zur letzten Pause bzw. zum Stopp aufgelaufene aktive Sekunden.
        last_resumed_at:
          type:
            - string
            - "null"
          format: date-time
        billability:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
          description: Override; null = von Projekt/Kunde erben.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        active_seconds:
          type: integer
          description: Servergerechnete aktive Dauer (ohne Pausen) zum Antwortzeitpunkt.
    TimerCreate:
      type: object
      properties:
        client_id:
          type:
            - string
            - "null"
          format: uuid
          description: Idempotenzschlüssel; wird zur ID des Timers.
        project_id:
          type:
            - string
            - "null"
          format: uuid
          description: Aktives Projekt im Workspace (sonst 422).
        description:
          type: string
          maxLength: 2000
        started_at:
          type: string
          format: date-time
          description: ISO 8601 mit Zonen-Offset. Höchstens 5 Minuten in der Zukunft und fünf Jahre (5 × 366 Tage) in der Vergangenheit; Standard jetzt.
        ended_at:
          type: string
          format: date-time
          description: Nur für den Nachtrag. Nicht vor `started_at`, höchstens 5 Minuten in der Zukunft, Spanne ≤ 366 Tage.
        duration_seconds:
          type: integer
          minimum: 0
          maximum: 31622400
          description: Aktive Dauer des Nachtrags; nur zusammen mit `ended_at`, höchstens das Zeitfenster.
        billability:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
    TimerUpdate:
      type: object
      description: Mindestens eines der Felder; sonst 422.
      minProperties: 1
      properties:
        description:
          type: string
          maxLength: 2000
          description: '"" = leeren.'
        project_id:
          type:
            - string
            - "null"
          format: uuid
          description: null = Zuordnung lösen; sonst aktives Projekt im Workspace.
        billability:
          type:
            - string
            - "null"
          enum:
            - billable
            - non_billable
            - internal
            - excluded
            - null
        started_at:
          type: string
          format: date-time
          description: Nur bei gestopptem Timer.
        ended_at:
          type: string
          format: date-time
          description: Nur bei gestopptem Timer; nicht vor `started_at`.
        duration_seconds:
          type: integer
          minimum: 0
          maximum: 31622400
          description: Aktive Dauer; nur bei gestopptem Timer, höchstens das Zeitfenster.
    UrlResult:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          required:
            - url
          properties:
            url:
              type: string
              format: uri
    WorkspaceBranding:
      type: object
      required:
        - company_name
        - accent_color
        - footer_text
        - has_logo
      properties:
        company_name:
          type:
            - string
            - "null"
          description: Firmenname im Dokumentkopf (null → Workspace-Name).
        accent_color:
          type:
            - string
            - "null"
          pattern: ^#[0-9a-fA-F]{6}$
          description: Akzentfarbe der Dokumente (null → Standard-Akzent).
        footer_text:
          type:
            - string
            - "null"
          description: Fußzeilentext der PDF-Exporte.
        has_logo:
          type: boolean
          description: Ob ein Logo hinterlegt ist (Abruf über /workspace/branding/logo).
    WorkspaceMembership:
      type: object
      required:
        - id
        - name
        - role
        - active
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        role:
          type: string
          enum:
            - owner
            - member
            - viewer
        active:
          type: boolean
          description: true für den Workspace, gegen den die Anfrage gerade arbeitet.
    ProgressEntryInput:
      type: object
      properties:
        title:
          type: string
          maxLength: 200
        body:
          type: string
          maxLength: 2000
        kind:
          type: string
          enum:
            - new
            - improved
            - fixed
            - tech
            - alignment
        status:
          type: string
          enum:
            - done
            - in_progress
        happened_from:
          type:
            - string
            - "null"
          format: date
        happened_to:
          type:
            - string
            - "null"
          format: date
        week_start:
          type: string
          format: date
          description: Nur beim Anlegen; ein beliebiger Tag der Woche.
    ProgressEntryPatch:
      type: object
      minProperties: 1
      properties:
        title:
          type: string
          maxLength: 200
        body:
          type: string
          maxLength: 2000
        kind:
          type: string
          enum:
            - new
            - improved
            - fixed
            - tech
            - alignment
        status:
          type: string
          enum:
            - done
            - in_progress
        happened_from:
          type:
            - string
            - "null"
          format: date
        happened_to:
          type:
            - string
            - "null"
          format: date
        hidden:
          type: boolean
        sort:
          type: integer
    ProgressEntryRow:
      type: object
      required:
        - id
        - project_id
        - week_start
        - title
        - body
        - kind
        - status
        - origin
        - hidden
        - source_refs
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        week_start:
          type: string
          format: date
        title:
          type: string
        body:
          type: string
        kind:
          type: string
          enum:
            - new
            - improved
            - fixed
            - tech
            - alignment
        status:
          type: string
          enum:
            - done
            - in_progress
        happened_from:
          type:
            - string
            - "null"
          format: date
        happened_to:
          type:
            - string
            - "null"
          format: date
        sort:
          type: integer
        origin:
          type: string
          enum:
            - ai
            - ai_edited
            - manual
          description: ai = von der KI, unverändert; ai_edited = danach bearbeitet; manual = selbst angelegt.
        hidden:
          type: boolean
        generation_id:
          type:
            - string
            - "null"
          format: uuid
        source_refs:
          type: object
          properties:
            commits:
              type: array
              items:
                type: string
                format: uuid
            sessions:
              type: array
              items:
                type: string
                format: uuid
            timers:
              type: array
              items:
                type: string
                format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ProgressReport:
      type: object
      required:
        - generation_id
        - project
        - from
        - to
        - created_at
        - reused
        - summary
        - points
        - entry_count
        - covered_count
        - uncovered
        - effort
      properties:
        generation_id:
          type: string
          format: uuid
        project:
          type: object
          required:
            - id
            - name
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        created_at:
          type: string
          format: date-time
        reused:
          type: boolean
          description: Ergebnis eines früheren Laufs bei unveränderten Wochenpunkten.
        summary:
          type: string
        points:
          type: array
          items:
            type: object
            required:
              - title
              - body
              - kind
              - status
              - from
              - to
              - entry_ids
              - effort
            properties:
              title:
                type: string
              body:
                type: string
              kind:
                type: string
                enum:
                  - new
                  - improved
                  - fixed
                  - tech
                  - alignment
              status:
                type: string
                enum:
                  - done
                  - in_progress
              from:
                type:
                  - string
                  - "null"
                format: date
              to:
                type:
                  - string
                  - "null"
                format: date
              entry_ids:
                type: array
                description: Wochenpunkte, die in diesen Punkt eingehen.
                items:
                  type: string
                  format: uuid
              effort:
                $ref: "#/components/schemas/ProgressEffort"
        entry_count:
          type: integer
        covered_count:
          type: integer
        uncovered:
          type: array
          description: Wochenpunkte, die keinem Berichtspunkt zugeordnet sind.
          items:
            type: object
            required:
              - id
              - title
            properties:
              id:
                type: string
                format: uuid
              title:
                type: string
        effort:
          $ref: "#/components/schemas/ProgressEffort"
    ProgressEffort:
      type: object
      required:
        - human_seconds
        - ai_seconds
      properties:
        human_seconds:
          type: integer
        ai_seconds:
          type: integer
    ProgressList:
      type: object
      required:
        - enabled
        - from
        - to
        - weeks
      properties:
        enabled:
          type: boolean
          description: Workspace-Schalter Projektfortschritt.
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        weeks:
          type: array
          items:
            type: object
            required:
              - week_start
              - week_end
              - evidence_count
              - new_evidence_count
              - generation
              - entries
            properties:
              week_start:
                type: string
                format: date
              week_end:
                type: string
                format: date
              evidence_count:
                type: integer
                description: Belege mit Inhalt in der Woche.
              new_evidence_count:
                type: integer
                description: Belege
                die der letzte erfolgreiche Lauf noch nicht kannte.: null
              generation:
                oneOf:
                  - type: "null"
                  - type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        enum:
                          - running
                          - done
                          - failed
                      created_at:
                        type: string
                        format: date-time
                      ref_count:
                        type: integer
                      covered_count:
                        type: integer
                      error:
                        type:
                          - string
                          - "null"
              entries:
                type: array
                items:
                  allOf:
                    - $ref: "#/components/schemas/ProgressEntryRow"
                    - type: object
                      properties:
                        effort:
                          type: object
                          properties:
                            human_seconds:
                              type: integer
                            ai_seconds:
                              type: integer
                        sources:
                          type: array
                          items:
                            type: object
                            properties:
                              type:
                                type: string
                                enum:
                                  - commit
                                  - session
                                  - timer
                              id:
                                type: string
                                format: uuid
                              day:
                                type: string
                                format: date
                              label:
                                type: string
                              seconds:
                                type:
                                  - integer
                                  - "null"
    AiFeatureState:
      type: object
      required:
        - enabled
        - consent
      properties:
        enabled:
          type: boolean
        consent:
          description: Zustimmung beim Einschalten; null, solange die Funktion aus ist.
          oneOf:
            - type: "null"
            - type: object
              required:
                - at
                - by_name
              properties:
                at:
                  type: string
                  format: date-time
                by_name:
                  type:
                    - string
                    - "null"
                  description: Name (ersatzweise E-Mail) der Person, die eingeschaltet hat.
    WorkspaceSettings:
      type: object
      description: Account-weite Einstellungen des Workspaces.
      required:
        - invoice_retention_mode
        - ai_features
      properties:
        ai_features:
          type: object
          description: Schalter der KI-Funktionen; alle standardmäßig aus.
          required:
            - progress
            - session_titles
          properties:
            progress:
              $ref: "#/components/schemas/AiFeatureState"
            session_titles:
              $ref: "#/components/schemas/AiFeatureState"
        invoice_retention_mode:
          type: string
          enum:
            - audit
            - delete
          description: |
            `audit` (Standard): festgeschriebene Rechnungen werden nie gelöscht, Korrekturen laufen über einen Stornobeleg. `delete`: sie dürfen endgültig gelöscht werden. Entwürfe sind in beiden Modi löschbar.
