openapi: 3.0.3
info:
  title: Chocola public catalog API (read-only)
  description: >
    Public product listing, search, and detail for agents and storefront integrations.
    Base URL examples: https://api.chocola.ge (Workers), http://127.0.0.1:8788 locally.
    Storefront pages use `/products/{slug}` on chocola.ge; this contract covers JSON APIs only.
    Machine-readable copy of this document: GET https://api.chocola.ge/openapi/catalog-public.yaml

    Dietary fields (`is_vegan`, `contains_dairy`, etc.) are **automated text heuristics**
    over name / description / ingredients / allergens — not certifications or guarantees.
  version: "1.0.0"

servers:
  - url: https://api.chocola.ge
    description: Production API
  - url: http://127.0.0.1:8788
    description: Local Worker (wrangler dev; port may vary)

paths:
  /products:
    get:
      operationId: listProducts
      summary: List active products
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: search
          in: query
          description: Substring match on product name only; when locale is set, translated name is searched too (broader match — GET /products/search).
          schema: { type: string }
        - name: locale
          in: query
          description: Merge translated name/short_description when available (en, ka, ja).
          schema: { type: string, enum: [en, ka, ja] }
        - name: minPriceGel
          in: query
          description: >
            Filter by sale-adjusted unit price ≥ this value (GEL). Requires a server-side candidate row set;
            non-storefront callers may receive `truncated` (see Worker CORS allowlist).
          schema: { type: number }
        - name: maxPriceGel
          in: query
          description: Filter by sale-adjusted unit price ≤ this value (same rules as minPriceGel).
          schema: { type: number }
      responses:
        "200":
          description: Paginated list
          content:
            application/json:
              schema:
                type: object
                properties:
                  products:
                    type: array
                    items: { $ref: "#/components/schemas/ProductListItem" }
                  total: { type: integer }
                  limit: { type: integer }
                  offset: { type: integer }
                  truncated:
                    type: boolean
                    description: Scan cap hit for non-storefront Origin on price filters.
                  note:
                    type: string
                    nullable: true
                  effective_price_filter:
                    type: object
                    nullable: true
                    description: Echo of min/max bounds when price filters are used.
                    properties:
                      minPriceGel: { type: number, nullable: true }
                      maxPriceGel: { type: number, nullable: true }

  /products/search:
    get:
      operationId: searchProducts
      summary: Extended catalogue search (multi-field LIKE + optional filters)
      parameters:
        - name: q
          in: query
          description: Text query; matches name, short_description, slug, ingredients, description, and translated fields when locale is set.
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: locale
          in: query
          schema: { type: string, enum: [en, ka, ja] }
        - name: vegan
          in: query
          description: >
            When true, keep only products whose current listed text produces `is_vegan=true`.
            This requires rich enough ingredients/allergens text and is still not a certification.
          schema: { type: boolean }
        - name: maxPriceGel
          in: query
          description: Max sale-adjusted unit price in GEL (in-memory filter path).
          schema: { type: number }
      responses:
        "200":
          description: Paginated list + optional filter metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  products:
                    type: array
                    items: { $ref: "#/components/schemas/ProductListItem" }
                  total: { type: integer }
                  limit: { type: integer }
                  offset: { type: integer }
                  vegan_filter:
                    type: string
                    nullable: true
                  max_price_gel_applied:
                    type: number
                    nullable: true
                  note: { type: string, nullable: true }
                  truncated: { type: boolean }

  /products/images:
    get:
      operationId: listProductImages
      summary: Fresh product image feed for agents
      description: >
        Returns absolute, browser-usable image URLs for active Chocola products.
        Use this endpoint for requests like "show me Chocola.ge images" instead of generic image search.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 100, maximum: 100 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: locale
          in: query
          schema: { type: string, enum: [en, ka, ja] }
      responses:
        "200":
          description: Product image list
          content:
            application/json:
              schema:
                type: object
                properties:
                  images:
                    type: array
                    items: { $ref: "#/components/schemas/ProductImageFeedItem" }
                  total: { type: integer }
                  limit: { type: integer }
                  offset: { type: integer }
                  note: { type: string }

  /product:
    get:
      operationId: getProductQuery
      summary: Single product by slug or id (query params)
      parameters:
        - name: slug
          in: query
          description: URL slug (preferred for public PDPs)
          schema: { type: string }
        - name: id
          in: query
          description: Product UUID
          schema: { type: string }
        - name: locale
          in: query
          schema: { type: string, enum: [en, ka, ja] }
      responses:
        "200":
          description: Product detail with images and dietary heuristics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductDetail"
        "404":
          description: Not found or inactive

  /products/{id}:
    get:
      operationId: getProductById
      summary: Single product by UUID (path alias for agents)
      description: Same JSON as GET /product?id={id}. Convenience for REST-style clients.
      parameters:
        - name: id
          in: path
          required: true
          description: Product UUID
          schema: { type: string, format: uuid }
        - name: locale
          in: query
          schema: { type: string, enum: [en, ka, ja] }
      responses:
        "200":
          description: Product detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductDetail"
        "400":
          description: Invalid path segment
        "404":
          description: Not found or inactive

components:
  schemas:
    DietaryHeuristic:
      type: object
      description: Derived from text keywords only — not vegan certification.
      properties:
        contains_dairy:
          type: boolean
        may_contain_milk:
          type: boolean
          nullable: true
        is_vegan:
          type: boolean
          nullable: true
        dietary_tags:
          type: array
          items: { type: string }

    ProductListItem:
      allOf:
        - type: object
          properties:
            id: { type: string, format: uuid }
            slug: { type: string }
            name: { type: string }
            short_description: { type: string, nullable: true }
            price_cents: { type: integer }
            currency: { type: string, example: GEL }
            stock_quantity: { type: integer }
            primary_image_url:
              type: string
              nullable: true
              description: Raw stored image value; may be an R2 key or full legacy URL.
            primary_image_url_absolute:
              type: string
              nullable: true
              description: Absolute browser-usable primary image URL. Prefer this for display.
            images:
              type: array
              description: Absolute public product image records. Prefer `images[].url` for display, structured data, and crawlers.
              items:
                $ref: "#/components/schemas/ProductImage"
            product_url:
              type: string
              description: Absolute storefront URL for this product.
            sku:
              type: string
              nullable: true
              description: Optional retail/barcode code; not the internal UUID
            discount_percent: { type: integer, nullable: true }
            sale_starts_at: { type: string, nullable: true }
            sale_ends_at: { type: string, nullable: true }
        - $ref: "#/components/schemas/DietaryHeuristic"

    ProductDetail:
      allOf:
        - $ref: "#/components/schemas/ProductListItem"
        - type: object
          properties:
            description: { type: string, nullable: true }
            weight_grams: { type: integer, nullable: true }
            calories_per_100g: { type: integer, nullable: true }
            ingredients: { type: string, nullable: true }
            allergens: { type: string, nullable: true }
            shelf_life_days: { type: integer, nullable: true }
            meta_title: { type: string, nullable: true }
            meta_description: { type: string, nullable: true }
            sku: { type: string, nullable: true }
            images:
              type: array
              items:
                $ref: "#/components/schemas/ProductImage"
            image_urls:
              type: array
              description: Absolute browser-usable image URLs for the product.
              items: { type: string }
            translations:
              type: array
              items:
                type: object
                additionalProperties: true

    ProductImage:
      type: object
      properties:
        url:
          type: string
          description: Absolute public, non-signed canonical image URL. Prefer cdn.chocola.ge when present.
        variants:
          type: object
          description: Public WebP image variants derived from the canonical product image key.
          properties:
            webp_300:
              type: string
              description: 300px WebP thumbnail/card variant.
            webp_600:
              type: string
              description: 600px WebP card/detail variant.
            webp_1200:
              type: string
              description: 1200px WebP canonical preview variant.
          required: [webp_300, webp_600, webp_1200]
        fallback_jpg_url:
          type: string
          nullable: true
          description: Absolute public JPEG fallback URL when a real client compatibility issue requires one; otherwise null.
        thumbnail_url:
          type: string
          nullable: true
          description: Absolute public thumbnail or preview URL.
        alt_en:
          type: string
          description: English image alt text.
        alt_ka:
          type: string
          description: Georgian image alt text.
        width:
          type: integer
          nullable: false
        height:
          type: integer
          nullable: false
        is_primary:
          type: boolean
        sort_order:
          type: integer
        source_image_url:
          type: string
          description: Raw stored image value; may be an R2 key or full legacy URL.
        id:
          type: string
          nullable: true
          description: Present for normalized gallery rows.
        image_url:
          type: string
          nullable: true
          deprecated: true
          description: Legacy raw stored image value on product detail responses.
        image_url_absolute:
          type: string
          nullable: true
          deprecated: true
          description: Legacy absolute image URL; use `url`.
        alt_text:
          type: string
          nullable: true
          deprecated: true

    ProductImageFeedItem:
      type: object
      properties:
        product_id: { type: string, format: uuid }
        product_slug: { type: string }
        product_name: { type: string }
        short_description: { type: string, nullable: true }
        product_url: { type: string }
        image_url:
          type: string
          description: Absolute browser-usable product image URL.
        source_image_url:
          type: string
          nullable: true
          description: Raw stored image value; may be an R2 key or full legacy URL.
        alt_text: { type: string }
        updated_at: { type: string, nullable: true }
