openapi: 3.1.0
x-document-control:
  stand: "09.08.2026 15:33"
  branch: main
  status: historisch
  replaced_by: ./openapi-uc00.yaml
  change_history:
    - version: "1.1.1-doc"
      date_time: "09.08.2026 15:33"
      change: "Als historischen API-Block gekennzeichnet; verbindlicher UC00-Vertrag in openapi-uc00.yaml"
      author: Codex

info:
  title: "DiveLogix360 API – Block 2: Tenant-Management (SuperAdmin)"
  version: 1.1.0
  contact:
    name: DiveLogix360 Support
    url: https://divelogix360.ch
  description: |
    > **Historisch:** Dieser Block bleibt als Arbeitsstand erhalten. Fuer UC00 ist
    > `openapi-uc00.yaml` verbindlich.

    ## Block 2: Tenant-Management

    Dieser Block beschreibt alle API-Endpoints zur Verwaltung von Tenants
    durch den SuperAdmin.

    **Zugriff:** Ausschliesslich SuperAdmin. Alle Endpoints sind mit
    `BearerAuth` (JWT) gesichert und prüfen die Rolle `superadmin`.

    **V1-Scope:** Tenants werden manuell durch den SuperAdmin angelegt.
    Self-Service-Registrierung ist für V2 geplant.

    **Tenant-Lebenszyklus:**
    ```
    pending → active → suspended → deactivated
                ↑__________↓
    ```
    - `pending`     : Tenant angelegt, noch nicht aktiviert
    - `active`      : Tenant aktiv, alle Funktionen verfügbar
    - `suspended`   : Temporär gesperrt (z.B. Zahlungsverzug)
    - `deactivated` : Dauerhaft deaktiviert (kein Login möglich)

    **Telefon-Pflicht:**
    Mindestens `contact_phone_fixed` oder `contact_phone_mobile` muss
    angegeben werden. Beide Felder sind optional, aber nicht beide
    gleichzeitig leer.

    **Änderungshistorie:**
    - v1.1.0: `contact_phone` aufgeteilt in `contact_phone_fixed` + `contact_phone_mobile`

servers:
  - url: https://api.divelogix360.ch/v1
    description: Produktion (Hetzner Cloud, Go-Live)
  - url: https://api-dev.divelogix360.ch/v1
    description: Entwicklung / Staging

tags:
  - name: Tenants
    description: Tenant-Verwaltung (nur SuperAdmin)

# ============================================================
# PATHS
# ============================================================

paths:

  /tenants:
    post:
      tags: [Tenants]
      summary: Neuen Tenant anlegen
      description: |
        Legt einen neuen Tenant (Tauchschule) an.

        **Ablauf:**
        1. SuperAdmin füllt Stammdaten aus (Name, Kontakt, Adresse, Plan)
        2. System generiert `tenant_id` (UUID) und zufälligen 8-stelligen Slug
        3. Tenant wird mit Status `pending` angelegt
        4. SuperAdmin aktiviert Tenant manuell via `PATCH /tenants/{tenantId}/status`
        5. Tenant-Admin erhält Einladungs-E-Mail (Invitation-Token, 72h)

        **Slug:** Zufällig generiert (8 alphanumerische Zeichen), nicht aus Kundendaten
        abgeleitet (Sicherheit, keine internen IDs in URLs).

        **Telefon-Pflicht:** Mindestens `contact_phone_fixed` oder
        `contact_phone_mobile` muss angegeben werden.
      operationId: createTenant
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTenantRequest'
            example:
              name: "Tauchschule Muster AG"
              slug: null
              contact_email: "info@tauchschule-muster.ch"
              contact_phone_fixed: "+41 44 123 45 67"
              contact_phone_mobile: null
              address:
                street: "Musterstrasse 12"
                city: "Zürich"
                postal_code: "8001"
                country: "CH"
              plan_type: "starter"
              trial_ends_at: null
              trial_max_users: null
              trial_max_customers: null
              trial_max_equipment: null
              admin_email: "admin@tauchschule-muster.ch"
              admin_first_name: "Max"
              admin_last_name: "Muster"
      responses:
        '201':
          description: Tenant erfolgreich angelegt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
              example:
                id: "b7e2d4f1-1234-5678-abcd-ef0123456789"
                slug: "a3f9b2c1"
                name: "Tauchschule Muster AG"
                status: "pending"
                contact_email: "info@tauchschule-muster.ch"
                contact_phone_fixed: "+41 44 123 45 67"
                contact_phone_mobile: null
                address:
                  street: "Musterstrasse 12"
                  city: "Zürich"
                  postal_code: "8001"
                  country: "CH"
                plan_type: "starter"
                trial_ends_at: null
                trial_max_users: null
                trial_max_customers: null
                trial_max_equipment: null
                created_at: "2024-01-15T10:30:00Z"
                updated_at: "2024-01-15T10:30:00Z"
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Tenant mit dieser E-Mail oder diesem Slug existiert bereits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "tenant_already_exists"
                message: "Ein Tenant mit dieser E-Mail-Adresse existiert bereits."
                status: 409
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      tags: [Tenants]
      summary: Alle Tenants auflisten
      description: |
        Gibt eine paginierte Liste aller Tenants zurück.

        **Filter:** Status, Plan-Typ, Suchbegriff (Name, E-Mail)
        **Sortierung:** `created_at`, `name`, `status` (auf- oder absteigend)
        **Pagination:** Cursor-basiert via `after` Parameter
      operationId: listTenants
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, active, suspended, deactivated]
        - name: plan_type
          in: query
          schema:
            type: string
            enum: [test, starter, professional, enterprise]
        - name: search
          in: query
          schema:
            type: string
        - name: sort_by
          in: query
          schema:
            type: string
            enum: [created_at, name, status]
            default: created_at
        - name: sort_dir
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: after
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Liste der Tenants
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /tenants/{tenantId}:
    parameters:
      - $ref: '#/components/parameters/TenantId'

    get:
      tags: [Tenants]
      summary: Einzelnen Tenant abrufen
      description: Gibt alle Stammdaten eines Tenants zurück inkl. Plan und Trial-Overrides.
      operationId: getTenant
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Tenant-Daten
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    patch:
      tags: [Tenants]
      summary: Tenant-Stammdaten aktualisieren
      description: |
        Aktualisiert Stammdaten eines Tenants (Name, Kontakt, Adresse).

        **Partial Update:** Nur übermittelte Felder werden geändert.

        **Nicht änderbar via diesem Endpoint:**
        - `status` → via `PATCH /tenants/{tenantId}/status`
        - `plan_type` und Trial-Overrides → via `PATCH /tenants/{tenantId}/plan`
        - `slug` → unveränderlich nach Erstellung

        **Telefon-Pflicht:** Wenn Telefonnummern aktualisiert werden, muss
        mindestens eine der beiden (fixed oder mobile) gefüllt bleiben.
      operationId: updateTenant
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenantRequest'
            example:
              name: "Tauchschule Muster GmbH"
              contact_phone_fixed: "+41 44 999 88 77"
              contact_phone_mobile: "+41 79 999 88 77"
      responses:
        '200':
          description: Tenant erfolgreich aktualisiert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /tenants/{tenantId}/status:
    patch:
      tags: [Tenants]
      summary: Tenant-Status ändern
      description: |
        Ändert den Status eines Tenants.

        **Erlaubte Übergänge:**
        | Von          | Nach         | Beschreibung                          |
        |--------------|--------------|---------------------------------------|
        | pending      | active       | Tenant erstmalig aktivieren           |
        | active       | suspended    | Temporär sperren                      |
        | suspended    | active       | Sperre aufheben                       |
        | active       | deactivated  | Dauerhaft deaktivieren                |
        | suspended    | deactivated  | Dauerhaft deaktivieren                |

        `deactivated` ist final — kein Rücksprung möglich.
      operationId: updateTenantStatus
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TenantId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenantStatusRequest'
            examples:
              activate:
                summary: Tenant aktivieren
                value:
                  status: "active"
                  reason: "Onboarding abgeschlossen, Zahlung bestätigt."
              suspend:
                summary: Tenant sperren
                value:
                  status: "suspended"
                  reason: "Rechnung 2024-001 seit 30 Tagen offen."
      responses:
        '200':
          description: Status erfolgreich geändert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantStatusResponse'
        '400':
          description: Ungültiger Statusübergang
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "invalid_status_transition"
                message: "Übergang von 'deactivated' zu 'active' ist nicht erlaubt."
                status: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /tenants/{tenantId}/plan:
    parameters:
      - $ref: '#/components/parameters/TenantId'

    get:
      tags: [Tenants]
      summary: Plan-Informationen abrufen
      operationId: getTenantPlan
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Plan-Informationen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantPlanResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    patch:
      tags: [Tenants]
      summary: Plan und/oder Trial-Overrides ändern
      description: |
        Ändert den Plan-Typ und/oder setzt Trial-Override-Limits.

        **Felder und ihre Standardwerte pro Plan:**
        | Plan         | max_users | max_customers | max_equipment |
        |--------------|-----------|---------------|---------------|
        | test         | 1         | 2             | 5             |
        | starter      | 2         | unbegrenzt    | 50            |
        | professional | 10        | unbegrenzt    | unbegrenzt    |
        | enterprise   | unbegrenzt| unbegrenzt    | unbegrenzt    |

        `null` bei Override = Plan-Standard gilt wieder.
      operationId: updateTenantPlan
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenantPlanRequest'
            examples:
              plan_upgrade:
                summary: Plan upgraden
                value:
                  plan_type: "professional"
              trial_extension:
                summary: Trial verlängern
                value:
                  plan_type: "test"
                  trial_ends_at: "2024-03-01T00:00:00Z"
                  trial_max_users: 3
                  trial_max_customers: 10
                  trial_max_equipment: 20
              override_reset:
                summary: Alle Overrides zurücksetzen
                value:
                  trial_ends_at: null
                  trial_max_users: null
                  trial_max_customers: null
                  trial_max_equipment: null
      responses:
        '200':
          description: Plan erfolgreich geändert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantPlanResponse'
        '400':
          description: Validierungsfehler oder Downgrade-Konflikt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "plan_downgrade_conflict"
                message: "Downgrade nicht möglich: Tenant hat 8 Benutzer, Plan 'starter' erlaubt max. 2."
                status: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

# ============================================================
# COMPONENTS
# ============================================================

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT Access Token (RS256). Rolle `superadmin` erforderlich.

  parameters:
    TenantId:
      name: tenantId
      in: path
      required: true
      schema:
        type: string
        format: uuid

  schemas:

    CreateTenantRequest:
      type: object
      required: [name, contact_email, plan_type, admin_email, admin_first_name, admin_last_name]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        slug:
          type: string
          nullable: true
          description: Wenn null — automatisch generiert (empfohlen)
        contact_email:
          type: string
          format: email
        contact_phone_fixed:
          type: string
          nullable: true
          description: Festnetznummer. Mindestens fixed oder mobile erforderlich.
        contact_phone_mobile:
          type: string
          nullable: true
          description: Mobilnummer. Mindestens fixed oder mobile erforderlich.
        address:
          $ref: '#/components/schemas/Address'
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        trial_max_users:
          type: integer
          nullable: true
        trial_max_customers:
          type: integer
          nullable: true
        trial_max_equipment:
          type: integer
          nullable: true
        admin_email:
          type: string
          format: email
        admin_first_name:
          type: string
        admin_last_name:
          type: string

    UpdateTenantRequest:
      type: object
      description: Partial Update. Bei Telefon-Update mind. eines der beiden Felder gefüllt lassen.
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        contact_email:
          type: string
          format: email
        contact_phone_fixed:
          type: string
          nullable: true
        contact_phone_mobile:
          type: string
          nullable: true
        address:
          $ref: '#/components/schemas/Address'

    UpdateTenantStatusRequest:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [active, suspended, deactivated]
        reason:
          type: string
          nullable: true
          maxLength: 500

    UpdateTenantPlanRequest:
      type: object
      properties:
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]
          nullable: true
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        trial_max_users:
          type: integer
          minimum: 1
          nullable: true
        trial_max_customers:
          type: integer
          minimum: 1
          nullable: true
        trial_max_equipment:
          type: integer
          minimum: 1
          nullable: true

    TenantResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          example: "a3f9b2c1"
        name:
          type: string
        status:
          type: string
          enum: [pending, active, suspended, deactivated]
        contact_email:
          type: string
          format: email
        contact_phone_fixed:
          type: string
          nullable: true
        contact_phone_mobile:
          type: string
          nullable: true
        address:
          $ref: '#/components/schemas/Address'
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        trial_max_users:
          type: integer
          nullable: true
        trial_max_customers:
          type: integer
          nullable: true
        trial_max_equipment:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TenantListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TenantListItem'
        pagination:
          $ref: '#/components/schemas/Pagination'

    TenantListItem:
      type: object
      description: Kurzform für Listen
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        name:
          type: string
        status:
          type: string
          enum: [pending, active, suspended, deactivated]
        contact_email:
          type: string
          format: email
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TenantStatusResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, active, suspended, deactivated]
        previous_status:
          type: string
          enum: [pending, active, suspended, deactivated]
        changed_at:
          type: string
          format: date-time
        changed_by:
          type: string
          format: email
        reason:
          type: string
          nullable: true

    TenantPlanResponse:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        plan_type:
          type: string
          enum: [test, starter, professional, enterprise]
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        overrides:
          type: object
          properties:
            trial_max_users:
              type: integer
              nullable: true
            trial_max_customers:
              type: integer
              nullable: true
            trial_max_equipment:
              type: integer
              nullable: true
        effective_limits:
          type: object
          description: null = unbegrenzt
          properties:
            max_users:
              type: integer
              nullable: true
            max_customers:
              type: integer
              nullable: true
            max_equipment:
              type: integer
              nullable: true
        plan_features:
          type: array
          items:
            type: object
            properties:
              usecase_type:
                type: string
              is_included:
                type: boolean

    Address:
      type: object
      properties:
        street:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        postal_code:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
          enum: [CH, DE, AT]

    Pagination:
      type: object
      properties:
        total:
          type: integer
        limit:
          type: integer
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true

    ErrorResponse:
      type: object
      required: [error, message, status]
      properties:
        error:
          type: string
        message:
          type: string
        status:
          type: integer

    ValidationErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  code:
                    type: string
                  message:
                    type: string

  responses:

    ValidationError:
      description: Validierungsfehler
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'

    Unauthorized:
      description: Nicht authentifiziert
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "unauthorized"
            message: "Authentifizierung erforderlich."
            status: 401

    Forbidden:
      description: Keine Berechtigung
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "forbidden"
            message: "Zugriff verweigert. SuperAdmin-Rolle erforderlich."
            status: 403

    NotFound:
      description: Ressource nicht gefunden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "not_found"
            message: "Tenant nicht gefunden."
            status: 404

    InternalServerError:
      description: Interner Serverfehler
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "internal_server_error"
            message: "Ein unerwarteter Fehler ist aufgetreten."
            status: 500
