openapi: 3.1.0
info:
  title: AS-INTER-BOX Catalogue API
  version: "1.0.0"
  description: |
    Publiczne, tylko-do-odczytu API katalogu opakowań AS-INTER-BOX
    (piekarnie, cukiernie, catering).

    - Brak autoryzacji, CORS `*`, odpowiedzi cache'owane 5 minut.
    - **B2B: API nie zawiera cen** — po wycenę należy kierować do formularza kontaktowego.
    - Dane pochodzą ze snapshotu katalogu generowanego przy każdym buildzie strony
      (pole `generatedAt` w odpowiedzi `GET /`).
    - Teksty kategorii dostępne w PL / EN / DE / UK (parametr `locale`).
  contact:
    name: AS-INTER-BOX
    url: https://www.as-inter-box.pl/kontakt/
  license:
    name: Proprietary — dane katalogowe AS-INTER-BOX
servers:
  - url: https://rhenyaldfpjahgjcyrxu.supabase.co/functions/v1/catalog-api
    description: Produkcja
tags:
  - name: Meta
  - name: Categories
  - name: Products
paths:
  /:
    get:
      tags: [Meta]
      summary: Informacje o API
      description: Zwraca wersję API, datę wygenerowania snapshotu, liczbę produktów i listę endpointów.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string, example: AS-INTER-BOX catalogue API }
                  version: { type: string, example: "1" }
                  generatedAt: { type: string, format: date-time }
                  products: { type: integer, example: 410 }
                  endpoints:
                    type: object
                    additionalProperties: { type: string }
  /health:
    get:
      tags: [Meta]
      summary: Status usługi i wersja snapshotu katalogu
      description: Publiczny health check. Zwraca status usługi oraz wersję (datę wygenerowania) snapshotu katalogu.
      responses:
        "200":
          description: Usługa sprawna
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  service: { type: string, example: catalog-api }
                  apiVersion: { type: string, example: "1" }
                  snapshot:
                    type: object
                    properties:
                      version: { type: string, example: "2026-08-13" }
                      generatedAt: { type: string, example: "2026-08-13" }
                      products: { type: integer, example: 410 }
                      categories: { type: integer, example: 21 }
                  time: { type: string, format: date-time }
        "503":
          description: Snapshot katalogu pusty (status degraded)
  /categories:
    get:
      tags: [Categories]
      summary: Lista kategorii katalogu
      parameters:
        - $ref: "#/components/parameters/Locale"
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "304":
          $ref: "#/components/responses/NotModified"
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  ids:
                    type: array
                    description: Lista identyfikatorów kategorii (wartości parametru `category`).
                    items: { type: string }
                  categories:
                    type: array
                    items: { $ref: "#/components/schemas/Category" }
  /facets:
    get:
      tags: [Products]
      summary: Dostępne wartości filtrów z licznikami
      description: |
        Zwraca wszystkie dostępne wartości do filtrowania (kategorie, grupy, wymiary,
        cechy, dostępność) wraz z licznikami policzonymi dla bieżących wyników
        (`q` + aktywne filtry). Ten sam blok `facets` co w `GET /search`, ale bez listy
        produktów — przydatne do zbudowania panelu filtrów jednym lekkim żądaniem.
        Każdy facet liczony jest w trybie drill-down (z pominięciem własnego filtra).
      parameters:
        - name: q
          in: query
          description: Fraza wyszukiwania (min. 2 znaki). Pominięcie liczy facety dla całego katalogu.
          schema: { type: string, minLength: 2 }
        - name: category
          in: query
          description: Filtr kategorii. Powtarzalny lub CSV.
          schema: { type: string }
        - name: group
          in: query
          schema: { type: string }
        - name: dimension
          in: query
          schema: { type: string }
        - name: feature
          in: query
          schema: { type: string }
        - name: availability
          in: query
          schema: { type: string, enum: [available, unavailable] }
        - name: facetLimit
          in: query
          description: Maksymalna liczba kubełków w każdym facecie.
          schema: { type: integer, minimum: 1, maximum: 500, default: 200 }
        - $ref: "#/components/parameters/Locale"
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "304":
          $ref: "#/components/responses/NotModified"
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [total, facets]
                properties:
                  query: { type: [string, "null"] }
                  filters: { $ref: "#/components/schemas/SearchFilters" }
                  total:
                    type: integer
                    description: Liczba produktów pasujących do `q` i aktywnych filtrów.
                  facetLimit: { type: integer, example: 200 }
                  facets: { $ref: "#/components/schemas/Facets" }
        "400":
          description: Nieprawidłowe parametry (`q` < 2 znaki lub nieznana wartość filtra)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /search:
    get:
      tags: [Products]
      summary: Wyszukiwanie produktów (ze stronicowaniem)
      description: |
        Odpowiedź zawiera metadane paginacji: `total`, `count`, `limit`, `offset`,
        `nextOffset`, `prevOffset` oraz `hasMore` — iteruj, dopóki `nextOffset` nie jest `null`.

        Ta sama logika co wyszukiwarka na stronie: dopasowanie po indeksie, nazwie handlowej,
        wymiarach (również zakresy wysokości, np. `200x130x35-30` pasuje do zapytania `200x130x32`),
        nazwie grupy, cechach, nazwie kategorii (4 języki) oraz synonimach (np. `babeczki` → muffiny).
      parameters:
        - name: q
          in: query
          description: Fraza wyszukiwania (min. 2 znaki). Pominięcie zwraca cały katalog (stronicowany).
          schema: { type: string, minLength: 2 }
          example: 200x130
        - name: category
          in: query
          description: |
            Identyfikator kategorii z `GET /categories`. Parametr powtarzalny
            (`category=a&category=b`) lub lista CSV (`category=a,b`) — wartości łączone przez OR.
          schema: { type: string }
          example: tacki
        - name: group
          in: query
          description: |
            Filtr po nazwie grupy produktowej (wartości z `facets.groups`).
            Powtarzalny lub CSV; OR wewnątrz parametru, AND z pozostałymi filtrami.
          schema: { type: string }
        - name: dimension
          in: query
          description: Filtr po wymiarze (wartości z `facets.dimensions`). Powtarzalny lub CSV.
          schema: { type: string }
          example: 200x130x60
        - name: feature
          in: query
          description: Filtr po cesze produktu (wartości z `facets.features`). Powtarzalny lub CSV.
          schema: { type: string }
        - name: availability
          in: query
          description: Filtr dostępności.
          schema: { type: string, enum: [available, unavailable] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
        - name: dedupe
          in: query
          description: Usuwa powtórzone indeksy produktów z wyników.
          schema: { type: boolean, default: false }
        - name: sort
          in: query
          description: |
            Kolejność wyników: `relevance` (trafność wobec `q`; bez `q` naturalna kolejność katalogu),
            `name` (nazwa handlowa), `category` (nazwa kategorii w `locale`, potem indeks),
            `index` (indeks produktu), `dimension` (wymiar).
          schema:
            type: string
            enum: [relevance, name, category, index, dimension]
            default: relevance
          example: name
        - name: order
          in: query
          description: Kierunek sortowania.
          schema: { type: string, enum: [asc, desc], default: asc }
        - name: facets
          in: query
          description: |
            Zwraca blok `facets` (kategorie, grupy, wymiary, cechy, dostępność) policzony
            dla bieżącego zapytania — UI może filtrować bez ponownych pobrań.
          schema: { type: boolean, default: true }
        - name: facetLimit
          in: query
          description: Maksymalna liczba kubełków w każdym facecie.
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - $ref: "#/components/parameters/Locale"
        - $ref: "#/components/parameters/IfNoneMatch"

      responses:
        "304":
          $ref: "#/components/responses/NotModified"
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SearchResponse" }
        "400":
          description: |
            Nieprawidłowe parametry: `q` < 2 znaki, nieznane `sort`, `order`
            lub nieznana wartość filtra (`category`, `group`, `dimension`, `feature`, `availability`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /products:
    get:
      tags: [Products]
      summary: Alias dla /search
      description: Identyczne parametry i odpowiedź jak `GET /search`.
      parameters:
        - name: q
          in: query
          schema: { type: string, minLength: 2 }
        - name: category
          in: query
          schema: { type: string }
        - name: group
          in: query
          schema: { type: string }
        - name: dimension
          in: query
          schema: { type: string }
        - name: feature
          in: query
          schema: { type: string }
        - name: availability
          in: query
          schema: { type: string, enum: [available, unavailable] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
        - name: dedupe
          in: query
          schema: { type: boolean, default: false }
        - name: sort
          in: query
          schema:
            type: string
            enum: [relevance, name, category, index, dimension]
            default: relevance
        - name: order
          in: query
          schema: { type: string, enum: [asc, desc], default: asc }
        - name: facets
          in: query
          schema: { type: boolean, default: true }
        - name: facetLimit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - $ref: "#/components/parameters/Locale"
        - $ref: "#/components/parameters/IfNoneMatch"

      responses:
        "304":
          $ref: "#/components/responses/NotModified"
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SearchResponse" }
  /products/{index}:
    get:
      tags: [Products]
      summary: Szczegóły produktu wraz z galerią zdjęć
      parameters:
        - name: index
          in: path
          required: true
          description: Indeks produktu (bez rozróżniania wielkości liter).
          schema: { type: string }
          example: "1001"
        - $ref: "#/components/parameters/Locale"
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "304":
          $ref: "#/components/responses/NotModified"
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  product: { $ref: "#/components/schemas/Product" }
        "404":
          description: Nie znaleziono produktu
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
components:
  parameters:
    IfNoneMatch:
      name: If-None-Match
      in: header
      required: false
      description: |
        Zapamiętany walidator `ETag`. Gdy pasuje do bieżącej odpowiedzi, API zwraca
        `304 Not Modified` bez ciała. Obsługiwane: lista tagów po przecinku, `*`,
        forma silna i słaba (`W/"..."`).
      schema: { type: string }
    Locale:
      name: locale
      in: query
      description: |
        Język nazw kategorii. Pominięcie parametru zwraca obiekt ze wszystkimi językami
        (`{ "pl": "...", "en": "...", "de": "...", "uk": "..." }`).
      schema:
        type: string
        enum: [pl, en, de, uk]
      example: de
  headers:
    ETag:
      description: Słaby walidator odpowiedzi, np. `W/"h-1a2b3c"`.
      schema: { type: string }
  responses:
    NotModified:
      description: Not Modified — treść niezmieniona względem `If-None-Match`.
      headers:
        ETag:
          $ref: "#/components/headers/ETag"
  schemas:
    LocalizedName:
      oneOf:
        - type: string
        - type: object
          properties:
            pl: { type: string }
            en: { type: string }
            de: { type: string }
            uk: { type: string }
    Category:
      type: object
      properties:
        id: { type: string, example: tacki }
        name: { $ref: "#/components/schemas/LocalizedName" }
        products: { type: integer, example: 42 }
        url: { type: string, format: uri, example: "https://www.as-inter-box.pl/katalog/tacki/" }
    Product:
      type: object
      properties:
        index: { type: string, example: "1001" }
        tradeName: { type: string }
        dimension: { type: string, example: "200x130x35 mm" }
        dimensionInner: { type: string }
        quantityPerCarton: { type: string, example: "500 szt." }
        features: { type: string }
        available: { type: boolean }
        availabilityNote: { type: string }
        category:
          type: object
          properties:
            id: { type: string }
            name: { $ref: "#/components/schemas/LocalizedName" }
        group: { type: string }
        image:
          type: [string, "null"]
          format: uri
          description: Absolutny URL zdjęcia głównego.
        gallery:
          type: array
          description: Zwracane tylko w `GET /products/{index}`.
          items: { type: string, format: uri }
        url: { type: string, format: uri, description: Link do produktu na stronie. }
    SearchResponse:
      type: object
      description: Wyniki wyszukiwania wraz z metadanymi paginacji.
      required: [total, count, limit, offset, nextOffset, prevOffset, hasMore, results]
      properties:
        query: { type: [string, "null"] }
        category: { type: [string, "null"] }
        filters:
          $ref: "#/components/schemas/SearchFilters"
        sort: { type: string, enum: [relevance, name, category, index, dimension], example: name }
        order: { type: string, enum: [asc, desc], example: asc }
        total:
          type: integer
          description: Łączna liczba dopasowanych produktów (przed stronicowaniem).
          example: 137
        count:
          type: integer
          description: Liczba produktów zwróconych na tej stronie.
          example: 20
        limit: { type: integer, example: 20 }
        offset: { type: integer, example: 20 }
        nextOffset:
          type: [integer, "null"]
          description: Wartość `offset` dla kolejnej strony albo `null`, gdy to ostatnia strona.
          example: 40
        prevOffset:
          type: [integer, "null"]
          description: Wartość `offset` dla poprzedniej strony albo `null` na pierwszej stronie.
          example: 0
        hasMore:
          type: boolean
          description: Skrót dla `nextOffset !== null`.
        facets:
          $ref: "#/components/schemas/Facets"
        results:
          type: array
          items: { $ref: "#/components/schemas/Product" }
    SearchFilters:
      type: object
      description: Aktywne filtry facetowe w tym zapytaniu (po normalizacji i deduplikacji).
      properties:
        categories: { type: array, items: { type: string } }
        groups: { type: array, items: { type: string } }
        dimensions: { type: array, items: { type: string } }
        features: { type: array, items: { type: string } }
        availability: { type: array, items: { type: string, enum: [available, unavailable] } }
    FacetBucket:
      type: object
      required: [value, count]
      properties:
        value: { type: string, example: tacki }
        label:
          type: string
          description: Etykieta w języku z `locale` (tylko dla facetu `categories`).
          example: Tacki
        count: { type: integer, example: 42 }
    Facets:
      type: object
      description: |
        Agregaty policzone dla bieżącego zapytania — pozwalają zbudować filtry w UI
        bez dodatkowych żądań. Każdy facet liczony jest na wynikach frazy `q`
        z pominięciem własnego filtra (drill-down), a z uwzględnieniem pozostałych,
        więc zaznaczenie wartości nie zeruje alternatyw w tym samym facecie.
        Kubełki sortowane malejąco po `count`, przycięte do `facetLimit`.
      properties:
        categories:
          type: array
          items: { $ref: "#/components/schemas/FacetBucket" }
        groups:
          type: array
          items: { $ref: "#/components/schemas/FacetBucket" }
        dimensions:
          type: array
          items: { $ref: "#/components/schemas/FacetBucket" }
        features:
          type: array
          items: { $ref: "#/components/schemas/FacetBucket" }
        availability:
          type: array
          description: Kubełki `available` i `unavailable`.
          items: { $ref: "#/components/schemas/FacetBucket" }

    Error:
      type: object
      properties:
        error: { type: string }
        index: { type: string }
