openapi: 3.1.0
info:
  title: Bookitly Hold API
  version: 0.1.0
  summary: Property Management System für Hotels
  description: |
    Multi-Tenant-SaaS-API für den Hotelbetrieb. Alle Endpunkte sind per OAuth 2.0
    (Laravel Passport) abgesichert. Der aktive Hotel-Kontext wird nach dem Login
    aus der User-Tenant-Zuordnung oder per Tenant-Switch-Endpunkt gesetzt.

    **Zwei Plane-Ebenen:**

    - **Control-Plane** (`/api/control/*`): Login, User-Profil, Session-Management,
      Tenant-Registry. Läuft beim späteren Go-Live zentral auf `api.hold.bookitly.de`.
    - **Shard** (`/api/*` ohne `control/`): Business-Daten wie Properties, Zimmer,
      Raten, Reservierungen. Läuft auf dem dem User zugeordneten Shard — dessen
      Base-URL wird in der Login-Response als `api_base_url` zurückgegeben.

    **Authentifizierung:** Bearer-Token im `Authorization`-Header. Der Token wird
    vom Login-Endpoint ausgestellt und läuft nach 24 Stunden ab.

  contact:
    email: mp@mbbg.de
    name: Marco Pardun / Bookitly

servers:
  - url: https://api.hold.bookitly.de
    description: Produktion / Control-Plane + aktueller Shard (Dev)
  - url: https://dev.hold.bookitly.de
    description: Legacy-Dev-URL (Control + Shard zusammen)

tags:
  - name: Authentication
    description: Login, Logout, Profil
  - name: Session
    description: Tenant-Wechsel im aktiven User-Kontext
  - name: Properties
    description: Hotels (Property-Entität). Eine Property ist eine konkrete Hotelimmobilie unter einem Tenant.
  - name: Unit Groups
    description: Zimmerkategorien unter einer Property (z.B. "Doppelzimmer Komfort"). Unter einer UnitGroup liegen mehrere konkrete Units.
  - name: Units
    description: Einzelne Zimmer. Jedes Zimmer gehoert zu einer UnitGroup (Kategorie) und damit zu einer Property.
  - name: Rate Plans
    description: Tarifplaene (z.B. "Flexibel", "Non-Refundable"). 1:1 an eine UnitGroup gebunden.
  - name: System
    description: Health-Checks, Meta-Infos

security:
  - bearerAuth: []

paths:
  /api/health:
    get:
      tags: [System]
      summary: Health-Check
      description: Gibt den Status der API zurück. Erfordert keine Authentifizierung.
      security: []
      responses:
        '200':
          description: API ist verfügbar
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /api/control/auth/login:
    post:
      tags: [Authentication]
      summary: Benutzer anmelden
      description: |
        Loggt einen User ein und gibt einen Bearer-Token zurück. Zusätzlich kommt
        im Response der aktive Tenant, alle verfügbaren Tenants für den User, und
        die Base-URL des zum aktiven Tenant gehörenden Shards.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login erfolgreich
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          description: E-Mail oder Passwort falsch
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: User hat kein aktives Tenant-Mapping
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Validierungsfehler
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '429':
          description: Rate-Limit überschritten (10 Versuche/Minute)

  /api/control/auth/me:
    get:
      tags: [Authentication]
      summary: Aktuelle Session abfragen
      description: Gibt User, aktiven Tenant und Shard-Info des aktuell authentifizierten Users zurück.
      responses:
        '200':
          description: Session-Daten
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionContext'
        '401':
          description: Nicht authentifiziert

  /api/control/auth/logout:
    post:
      tags: [Authentication]
      summary: Token widerrufen
      description: Revoke des aktuell verwendeten Bearer-Tokens. Der Token kann nach diesem Aufruf nicht mehr verwendet werden.
      responses:
        '200':
          description: Logout erfolgreich
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: logged_out

  /api/control/session/tenant:
    post:
      tags: [Session]
      summary: Aktiven Tenant wechseln
      description: |
        Wechselt den aktiven Tenant des Users. Setzt `is_default=true` auf die
        gegebene Tenant-Zuordnung (und damit `false` auf alle anderen). Die
        Response enthält die neue Shard-URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenant_id]
              properties:
                tenant_id:
                  type: string
                  format: uuid
                  example: 019d9e12-0a7c-7040-9dff-da5ee90408cf
      responses:
        '200':
          description: Tenant gewechselt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionContext'
        '403':
          description: User hat keinen Zugriff auf diesen Tenant
        '404':
          description: Tenant nicht gefunden

  /api/properties:
    get:
      tags: [Properties]
      summary: Alle Properties des aktiven Tenants auflisten
      responses:
        '200':
          description: Liste der Properties
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Property' }
    post:
      tags: [Properties]
      summary: Neue Property anlegen
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PropertyInput'
      responses:
        '201':
          description: Property angelegt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Property' }
        '422':
          description: Validierungsfehler

  /api/properties/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Properties]
      summary: Einzelne Property anzeigen
      responses:
        '200':
          description: Property-Details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Property' }
        '404':
          description: Nicht gefunden
    patch:
      tags: [Properties]
      summary: Property aktualisieren
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PropertyInput'
      responses:
        '200':
          description: Property aktualisiert
        '404':
          description: Nicht gefunden
    delete:
      tags: [Properties]
      summary: Property löschen (soft-delete)
      responses:
        '204':
          description: Gelöscht
        '404':
          description: Nicht gefunden

  /api/unit-groups:
    get:
      tags: [Unit Groups]
      summary: Alle Zimmerkategorien des aktiven Tenants auflisten
      description: Liefert alle UnitGroups, sortiert nach Position+Code. Optionaler Filter via ?property_id=.
      parameters:
        - name: property_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: Wenn gesetzt, nur UnitGroups dieser Property zurueckgeben.
      responses:
        '200':
          description: Liste der UnitGroups
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/UnitGroup' }
    post:
      tags: [Unit Groups]
      summary: Neue UnitGroup anlegen
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UnitGroupInput' }
      responses:
        '201':
          description: UnitGroup angelegt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/UnitGroup' }
        '422':
          description: Validierungsfehler (z.B. doppelter code in derselben Property)

  /api/unit-groups/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Unit Groups]
      summary: Einzelne UnitGroup anzeigen
      responses:
        '200':
          description: Details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/UnitGroup' }
        '404': { description: Nicht gefunden }
    patch:
      tags: [Unit Groups]
      summary: UnitGroup aktualisieren
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UnitGroupInput' }
      responses:
        '200': { description: Aktualisiert }
        '404': { description: Nicht gefunden }
    delete:
      tags: [Unit Groups]
      summary: UnitGroup löschen (soft-delete)
      responses:
        '204': { description: Gelöscht }
        '404': { description: Nicht gefunden }

  /api/units:
    get:
      tags: [Units]
      summary: Alle Zimmer des aktiven Tenants auflisten
      parameters:
        - name: property_id
          in: query
          required: false
          schema: { type: string, format: uuid }
        - name: unit_group_id
          in: query
          required: false
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Liste der Units
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Unit' }
    post:
      tags: [Units]
      summary: Neues Zimmer anlegen
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UnitInput' }
      responses:
        '201':
          description: Unit angelegt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Unit' }
        '422':
          description: Validierungsfehler

  /api/units/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Units]
      summary: Einzelnes Zimmer anzeigen
      responses:
        '200':
          description: Details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Unit' }
        '404': { description: Nicht gefunden }
    patch:
      tags: [Units]
      summary: Zimmer aktualisieren
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UnitInput' }
      responses:
        '200': { description: Aktualisiert }
        '404': { description: Nicht gefunden }
    delete:
      tags: [Units]
      summary: Zimmer löschen (soft-delete)
      responses:
        '204': { description: Gelöscht }
        '404': { description: Nicht gefunden }

  /api/rate-plans:
    get:
      tags: [Rate Plans]
      summary: Alle Tarifplaene des aktiven Tenants auflisten
      parameters:
        - name: unit_group_id
          in: query
          required: false
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Liste der RatePlans
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/RatePlan' }
    post:
      tags: [Rate Plans]
      summary: Neuen Tarifplan anlegen
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RatePlanInput' }
      responses:
        '201':
          description: RatePlan angelegt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/RatePlan' }

  /api/rate-plans/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Rate Plans]
      summary: Einzelnen Tarifplan anzeigen
      responses:
        '200':
          description: Details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/RatePlan' }
        '404': { description: Nicht gefunden }
    patch:
      tags: [Rate Plans]
      summary: Tarifplan aktualisieren
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RatePlanInput' }
      responses:
        '200': { description: Aktualisiert }
        '404': { description: Nicht gefunden }
    delete:
      tags: [Rate Plans]
      summary: Tarifplan löschen (soft-delete)
      responses:
        '204': { description: Gelöscht }
        '404': { description: Nicht gefunden }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Laravel-Passport-JWT. Wird vom Login-Endpoint ausgestellt.

  schemas:
    HealthResponse:
      type: object
      properties:
        status: { type: string, example: ok }
        time: { type: string, format: date-time }
        service: { type: string, example: bookitly-hold }
        env: { type: string, example: local }
        php: { type: string, example: 8.5.5 }
        laravel: { type: string, example: 13.5.0 }

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: mp@mbbg.de
        password:
          type: string
          format: password
          minLength: 6
          example: bookitly-dev-2026

    LoginResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT, im Authorization-Header als Bearer nutzen
        token_type:
          type: string
          enum: [Bearer]
        expires_at:
          type: string
          format: date-time
          nullable: true
        user: { $ref: '#/components/schemas/User' }
        active_tenant: { $ref: '#/components/schemas/TenantRef' }
        api_base_url:
          type: string
          format: uri
          description: Shard-URL für Business-Calls. Base für /api/properties etc.
        shard_id: { type: string }
        available_tenants:
          type: array
          items: { $ref: '#/components/schemas/TenantMembership' }

    SessionContext:
      type: object
      properties:
        user: { $ref: '#/components/schemas/User' }
        active_tenant: { $ref: '#/components/schemas/TenantRef' }
        api_base_url: { type: string, format: uri }
        shard_id: { type: string }
        available_tenants:
          type: array
          items: { $ref: '#/components/schemas/TenantMembership' }

    User:
      type: object
      properties:
        id: { type: integer, example: 1 }
        name: { type: string, example: Marco Pardun }
        email: { type: string, format: email }
        email_verified_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    TenantRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string, example: landhotel-lebensart }
        name: { type: string, example: Landhotel Lebensart }

    TenantMembership:
      allOf:
        - $ref: '#/components/schemas/TenantRef'
        - type: object
          properties:
            role:
              type: string
              example: owner
            is_default:
              type: boolean

    Property:
      type: object
      properties:
        id: { type: string, format: uuid }
        code:
          type: string
          maxLength: 16
          example: LLE
          description: Kurzcode für UI-Anzeige, unique pro Tenant
        name: { type: string, example: Landhotel Lebensart }
        short_description: { type: string, nullable: true }
        address:
          type: object
          nullable: true
          properties:
            street: { type: string }
            city: { type: string }
            postal_code: { type: string }
            country: { type: string, minLength: 2, maxLength: 2 }
        star_rating:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
        city_tax_configuration: { type: object, nullable: true }
        legal_reporting: { type: object, nullable: true }
        tax_configuration: { type: object, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    PropertyInput:
      type: object
      required: [code, name]
      properties:
        code: { type: string, maxLength: 16 }
        name: { type: string, maxLength: 255 }
        short_description: { type: string, nullable: true, maxLength: 2000 }
        address: { type: object, nullable: true }
        star_rating: { type: integer, nullable: true, minimum: 1, maximum: 5 }
        city_tax_configuration: { type: object, nullable: true }
        legal_reporting: { type: object, nullable: true }
        tax_configuration: { type: object, nullable: true }

    UnitGroup:
      type: object
      properties:
        id: { type: string, format: uuid }
        property_id: { type: string, format: uuid }
        code:
          type: string
          maxLength: 16
          example: DZK
          description: Kurzcode, unique innerhalb der Property (z.B. DZK fuer Doppelzimmer Komfort)
        name: { type: string, example: Doppelzimmer Komfort }
        description: { type: string, nullable: true }
        default_occupancy:
          type: integer
          minimum: 1
          maximum: 20
          example: 2
          description: Standard-Belegung in Anzahl Personen
        max_occupancy: { type: integer, minimum: 1, maximum: 20, example: 3 }
        min_occupancy: { type: integer, minimum: 1, maximum: 20, example: 1 }
        attributes:
          type: array
          nullable: true
          items: { type: string, maxLength: 40 }
          description: Feature-Tags wie "balkon", "meerblick", "non_smoking", "barrierefrei"
          example: [balkon, non_smoking]
        size_sqm:
          type: number
          format: float
          nullable: true
          minimum: 1
          maximum: 10000
          example: 22.5
          description: Zimmergroesse in Quadratmetern
        position:
          type: integer
          minimum: 0
          maximum: 9999
          example: 10
          description: Reihenfolge bei Anzeige in UI und Booking-Engine
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    UnitGroupInput:
      type: object
      required: [property_id, code, name]
      properties:
        property_id: { type: string, format: uuid }
        code: { type: string, maxLength: 16 }
        name: { type: string, maxLength: 255 }
        description: { type: string, nullable: true, maxLength: 2000 }
        default_occupancy: { type: integer, minimum: 1, maximum: 20 }
        max_occupancy: { type: integer, minimum: 1, maximum: 20 }
        min_occupancy: { type: integer, minimum: 1, maximum: 20 }
        attributes:
          type: array
          nullable: true
          items: { type: string, maxLength: 40 }
        size_sqm: { type: number, format: float, nullable: true }
        position: { type: integer, minimum: 0, maximum: 9999 }

    Unit:
      type: object
      properties:
        id: { type: string, format: uuid }
        property_id: { type: string, format: uuid }
        unit_group_id: { type: string, format: uuid }
        number:
          type: string
          maxLength: 16
          example: "12"
          description: Zimmernummer, unique innerhalb der Property
        label:
          type: string
          nullable: true
          maxLength: 255
          description: Optionales zusaetzliches Label
        floor:
          type: integer
          nullable: true
          minimum: -5
          maximum: 100
          description: Stockwerk (0 = EG, -1 = UG, 1..N = Obergeschosse)
          example: 1
        attributes:
          type: array
          nullable: true
          items: { type: string, maxLength: 40 }
          description: Override der UnitGroup-Attributes fuer dieses einzelne Zimmer
        is_active:
          type: boolean
          description: Ob das Zimmer aktuell buchbar ist
          example: true
        position: { type: integer, minimum: 0, maximum: 9999 }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    UnitInput:
      type: object
      required: [property_id, unit_group_id, number]
      properties:
        property_id: { type: string, format: uuid }
        unit_group_id: { type: string, format: uuid }
        number: { type: string, maxLength: 16 }
        label: { type: string, nullable: true, maxLength: 255 }
        floor: { type: integer, nullable: true, minimum: -5, maximum: 100 }
        attributes:
          type: array
          nullable: true
          items: { type: string, maxLength: 40 }
        is_active: { type: boolean }
        position: { type: integer, minimum: 0, maximum: 9999 }

    RatePlan:
      type: object
      properties:
        id: { type: string, format: uuid }
        unit_group_id: { type: string, format: uuid }
        code: { type: string, maxLength: 16, example: FLEX }
        name: { type: string, example: Flexibel }
        description: { type: string, nullable: true }
        board:
          type: string
          nullable: true
          enum: [NB, BB, HB, FB, AI]
          description: "NB=NoMeals, BB=Breakfast, HB=Halbpension, FB=Vollpension, AI=AllInclusive"
        cancellation_policy:
          type: object
          nullable: true
          description: |
            Freie Stornobedingung als JSON. Typ-Beispiele:
            - `{ "type": "flexible", "free_until_hours_before": 24 }`
            - `{ "type": "non-refundable" }`
            - `{ "type": "custom", "rules": [...], "text_de": "..." }`
          example:
            type: flexible
            free_until_hours_before: 24
        is_public:
          type: boolean
          description: Oeffentliche Rate (im Booking-Engine sichtbar)
        is_active: { type: boolean }
        position: { type: integer, minimum: 0, maximum: 9999 }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    RatePlanInput:
      type: object
      required: [unit_group_id, code, name]
      properties:
        unit_group_id: { type: string, format: uuid }
        code: { type: string, maxLength: 16 }
        name: { type: string, maxLength: 255 }
        description: { type: string, nullable: true, maxLength: 2000 }
        board: { type: string, nullable: true, enum: [NB, BB, HB, FB, AI] }
        cancellation_policy: { type: object, nullable: true }
        is_public: { type: boolean }
        is_active: { type: boolean }
        position: { type: integer, minimum: 0, maximum: 9999 }

    ErrorResponse:
      type: object
      properties:
        error: { type: string, example: invalid_credentials }
        message: { type: string, example: E-Mail oder Passwort ist falsch. }

    ValidationError:
      type: object
      properties:
        message: { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
