openapi: 3.0.3
info:
  title: LeanPIM API
  description: |
    Read-only Produkt-API (v1).

    **Authentifizierung:** Header `Authorization: Bearer <api_key>`.

    **Zugriff:** API-Keys können auf Lieferanten und Segmente eingeschränkt sein.
    IP-Blacklist / temporäre Blocklist nach fehlgeschlagenen Auth-Versuchen.

    **DEV-Keys:** Listen-Abrufe max. 10 Artikel pro Request (`meta.is_dev`, `meta.dev_max_per_page`).

    **WA:** Das Objekt `wa` erscheint in Detailantworten nur, wenn der Key
    „WA einschließen“ aktiviert hat. Felder sind Rohdaten (Semikolon-Listen).

    **Embed-Picker:** `POST /api/v1/embed/session` erzeugt ein kurzlebiges Token
    und eine `picker_url` für die UI-Auswahl; Übernahme per `postMessage`.
  version: "1.0.0"
  contact:
    name: LeanPIM

servers:
  - url: /
    description: Base-URL der LeanPIM-Installation

tags:
  - name: products
    description: Artikel suchen und abrufen
  - name: embed
    description: Kurzlebige Sessions für den Produkt-Picker (UI-Popup)

security:
  - ApiKeyBearer: []

paths:
  /api/v1/products:
    get:
      tags: [products]
      summary: Artikel auflisten / suchen
      description: |
        Gefilterte Artikelliste mit Pagination.
        `q` = schnelle Suche (Name, SKU, EAN, Ext-ID).
        `fulltext` = Volltext über Felder und Attribute.
      operationId: listProducts
      parameters:
        - $ref: '#/components/parameters/q'
        - $ref: '#/components/parameters/fulltext'
        - $ref: '#/components/parameters/skuQuery'
        - $ref: '#/components/parameters/eanQuery'
        - $ref: '#/components/parameters/supplierId'
        - $ref: '#/components/parameters/status'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/perPage'
      responses:
        '200':
          description: Artikelliste
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/products/{id}:
    get:
      tags: [products]
      summary: Artikel per ID
      description: Detail inkl. Preise, Bilder und Attribute. Optional `wa` je nach Key.
      operationId: getProductById
      parameters:
        - name: id
          in: path
          required: true
          description: Interne Artikel-ID
          schema:
            type: integer
            minimum: 1
            example: 1
      responses:
        '200':
          description: Artikeldetail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/products/by-sku/{sku}:
    get:
      tags: [products]
      summary: Artikel per SKU
      operationId: getProductBySku
      parameters:
        - name: sku
          in: path
          required: true
          description: Artikelnummer (SKU)
          schema:
            type: string
            example: SKU-1001
      responses:
        '200':
          description: Artikeldetail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/products/by-ean/{ean}:
    get:
      tags: [products]
      summary: Artikel per EAN
      operationId: getProductByEan
      parameters:
        - name: ean
          in: path
          required: true
          description: EAN / GTIN
          schema:
            type: string
            example: '4006381333931'
      responses:
        '200':
          description: Artikeldetail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/embed/session:
    post:
      tags: [embed]
      summary: Embed-Session für Produkt-Picker erzeugen
      description: |
        Erzeugt ein kurzlebiges Token (nicht der API-Key) und eine `picker_url`.
        Das Drittsystem öffnet die URL als Popup/iframe. Nach Auswahl sendet der Picker
        `postMessage` mit `type: leanpim.embed.transfer` und einem Artikel-JSON
        (ohne Roh-WA-Felder; bei WA-Konfiguration angereichert, SKU oft mit `-WA`).
        Optional `allowed_origin` setzen (Origin des Elternfensters).
      operationId: createEmbedSession
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                allowed_origin:
                  type: string
                  example: https://shop.example.com
                selection_key:
                  type: string
                  description: Optionaler Key einer früheren Übernahme (`embed.selection_key`), um Artikel/Menge/WA wieder zu öffnen
                  example: sel_ab12cd34…
      responses:
        '201':
          description: Session erzeugt
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      token:
                        type: string
                      expires_at:
                        type: string
                      ttl_minutes:
                        type: integer
                      picker_url:
                        type: string
                      allowed_origin:
                        type: string
                        nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

components:
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: API-Key
      description: API-Key aus der LeanPIM-Verwaltung als Bearer-Token.

  parameters:
    q:
      name: q
      in: query
      required: false
      description: Schnelle Suche (Name, SKU, EAN, Ext-ID)
      schema:
        type: string
        example: Stahl
    fulltext:
      name: fulltext
      in: query
      required: false
      description: Volltextsuche über Felder und Attribute
      schema:
        type: string
        example: blau Werkzeug
    skuQuery:
      name: sku
      in: query
      required: false
      description: SKU exakt
      schema:
        type: string
    eanQuery:
      name: ean
      in: query
      required: false
      description: EAN exakt
      schema:
        type: string
    supplierId:
      name: supplier_id
      in: query
      required: false
      description: Filter nach Lieferanten-ID (nur wenn der Key den Lieferanten erlaubt)
      schema:
        type: integer
        minimum: 1
    status:
      name: status
      in: query
      required: false
      description: Artikelstatus. Standard `active`. Leerer Wert = jeder Status (je nach Key-Zugriff).
      schema:
        type: string
        enum: [active, inactive, '']
        default: active
    page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
    perPage:
      name: per_page
      in: query
      required: false
      description: Einträge pro Seite. Normal max. 200 (Default 50). DEV-Keys max./Default 10.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50

  responses:
    Unauthorized:
      description: API-Key fehlt oder ungültig
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value: { error: 'API-Key fehlt' }
            invalid:
              value: { error: 'Ungültiger API-Key' }
    Forbidden:
      description: IP gesperrt (Blacklist oder Blocklist)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Artikel nicht gefunden oder durch Key-Scope nicht sichtbar
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Artikel nicht gefunden
    TooManyRequests:
      description: Quota / Rate-Limit des API-Keys überschritten
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string

    SupplierRef:
      type: object
      properties:
        id:
          type: integer
        code:
          type: string
          nullable: true
        name:
          type: string
          nullable: true

    ProductListItem:
      type: object
      properties:
        id:
          type: integer
        sku:
          type: string
          nullable: true
        ean:
          type: string
          nullable: true
        external_id:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        brand:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        status:
          type: string
          enum: [active, inactive]
        supplier:
          $ref: '#/components/schemas/SupplierRef'
        price_from:
          type: number
          nullable: true
          description: Niedrigster Staffelpreis (netto), falls vorhanden
        currency:
          type: string
          nullable: true
        primary_image:
          type: string
          nullable: true
          description: URL des Hauptbildes
        updated_at:
          type: string
          format: date-time
          nullable: true

    ProductListMeta:
      type: object
      properties:
        total:
          type: integer
        page:
          type: integer
        per_page:
          type: integer
        pages:
          type: integer
        is_dev:
          type: boolean
          description: Nur bei DEV-Keys
        dev_max_per_page:
          type: integer
          description: Nur bei DEV-Keys (aktuell 10)

    ProductListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ProductListItem'
        meta:
          $ref: '#/components/schemas/ProductListMeta'

    PriceTier:
      type: object
      properties:
        min_qty:
          type: number
          description: Ab-Menge der Staffel
        price_net:
          type: number
          nullable: true
        price_purchase:
          type: number
          nullable: true
        price_gross:
          type: number
          nullable: true
        currency:
          type: string
          nullable: true
        label:
          type: string
          nullable: true

    ProductImage:
      type: object
      properties:
        url:
          type: string
        path:
          type: string
          nullable: true
        sort_order:
          type: integer
        is_primary:
          type: boolean

    AttributeValue:
      type: object
      properties:
        label:
          type: string
        type:
          type: string
        value:
          nullable: true
          description: Wert je nach Attributtyp

    ProductWa:
      type: object
      description: |
        Werbeanbringung-Rohdaten (nur wenn Key `include_wa` aktiv).
        Viele Felder sind Semikolon-Listen aus dem Lieferanten-Export.
      properties:
        product_id:
          type: integer
        wa_options_code:
          type: string
          nullable: true
        wa_color_counts:
          type: string
          nullable: true
        wa_setup_cost_multiplier:
          type: string
          nullable: true
        wa_setup_costs:
          type: string
          nullable: true
        wa_price_tiers:
          type: string
          nullable: true
        wa_prices:
          type: string
          nullable: true
        wa_method_code:
          type: string
          nullable: true
        wa_options_code_2:
          type: string
          nullable: true
        wa_options_description:
          type: string
          nullable: true
        wa_position_code:
          type: string
          nullable: true
        wa_images:
          type: string
          nullable: true
        wa_position_description:
          type: string
          nullable: true
        wa_colors_max:
          type: string
          nullable: true
        wa_width_max:
          type: string
          nullable: true
        wa_height_max:
          type: string
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    ProductDetail:
      allOf:
        - $ref: '#/components/schemas/ProductListItem'
        - type: object
          properties:
            parent_sku:
              type: string
              nullable: true
            description:
              type: string
              nullable: true
            description_long:
              type: string
              nullable: true
            manufacturer:
              type: string
              nullable: true
            subcategory:
              type: string
              nullable: true
            product_group:
              type: string
              nullable: true
            dimensions_text:
              type: string
              nullable: true
            packaging:
              type: string
              nullable: true
            material:
              type: string
              nullable: true
            color:
              type: string
              nullable: true
            country_of_origin:
              type: string
              nullable: true
            customs_tariff:
              type: string
              nullable: true
            delivery_time:
              type: string
              nullable: true
            weight_text:
              type: string
              nullable: true
            price_base:
              type: number
              nullable: true
            search_weight:
              type: number
              nullable: true
            video_url:
              type: string
              nullable: true
            made_in_germany:
              nullable: true
            unit:
              type: string
              nullable: true
            weight_kg:
              type: number
              nullable: true
            length_mm:
              type: number
              nullable: true
            width_mm:
              type: number
              nullable: true
            height_mm:
              type: number
              nullable: true
            stock_quantity:
              type: number
              nullable: true
            min_order_qty:
              type: number
              nullable: true
            tax_rate:
              type: number
              nullable: true
            deactivated_at:
              type: string
              format: date-time
              nullable: true
            last_imported_at:
              type: string
              format: date-time
              nullable: true
            created_at:
              type: string
              format: date-time
              nullable: true
            prices:
              type: array
              items:
                $ref: '#/components/schemas/PriceTier'
            images:
              type: array
              items:
                $ref: '#/components/schemas/ProductImage'
            attributes:
              type: object
              additionalProperties:
                $ref: '#/components/schemas/AttributeValue'
              description: Map Attribut-Code → Wert
            wa:
              allOf:
                - $ref: '#/components/schemas/ProductWa'
              nullable: true
              description: Nur wenn API-Key WA einschließt

    ProductDetailResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/ProductDetail'
