openapi: 3.1.0
info:
  title: Ragently Custom Commerce Backend API
  version: 1.0.0
  description: >-
    Dit is het contract dat JOUW backend implementeert, niet een API die
    Ragently aanbiedt. Ragently's connector (APP_UTILS/commerce/custom_adapter.py
    + custom_client.py in de Ragently-codebase) roept deze endpoints
    server-to-server aan om orders, klanten, producten, checkouts en
    kennisbank-content van jouw webshop te ontsluiten voor de AI-chatbot.

    Werking in het kort:

    1. Jouw backend publiceert een manifest op
       `GET /.well-known/ragently` dat opgeeft welke capabilities (features)
       je ondersteunt.
    2. Ragently valideert dat manifest, filtert het op bekende capability-
       constanten en doet één lichte leesprobe om de koppeling te testen.
    3. Je hoeft ALLEEN de endpoints te implementeren die horen bij de
       capabilities die je in het manifest declareert. Endpoints van
       niet-gedeclareerde capabilities worden nooit aangeroepen.
    4. Voor GET-endpoints geldt de conventie: bestaat de resource niet, geef
       dan HTTP 404 terug (lege body is prima — alleen de statuscode telt).
       Voor schrijf-endpoints (POST/PUT) betekent 404 hetzelfde: resource
       niet gevonden; de body wordt in dat geval genegeerd.
    5. Elke andere niet-2xx-statuscode (4xx/5xx) wordt door de connector
       geïnterpreteerd als een fout; geef bij voorkeur een JSON-body met een
       `error`-veld mee (zie het `Error`-schema), al wordt de exacte inhoud
       niet machinaal geparsed buiten logging/foutmeldingen om.
    6. Alle 2xx-responses moeten `application/json` zijn.

    Authenticatie: Ragently authenticeert ZICHZELF bij jouw backend met
    precies één van de vier methodes hieronder (die je zelf kiest bij het
    koppelen: bearer-token, API-key-header, HTTP Basic, of OAuth2
    client-credentials). Zie `components.securitySchemes`.

    Dit document is 1-op-1 afgeleid van de daadwerkelijke adapter-code van
    de connector (custom_adapter.py) en de canonieke responseshapes daarin
    (base.py, CommerceClient) — er staan geen endpoints in die de connector
    niet ook echt aanroept.
servers:
  - url: "{baseUrl}"
    description: "De basis-URL van je eigen backend"
    variables:
      baseUrl:
        default: "https://your-backend.example.com"
        description: "Vul hier de basis-URL van je eigen (dev-)backend in om de endpoints live te testen."

tags:
  - name: manifest
    description: >-
      Het capability-manifest waarmee je backend bekendmaakt welke features
      (endpoints) hij ondersteunt. Altijd verplicht, ongeacht welke overige
      capabilities je declareert.
  - name: orders.read
    description: "Orderstatus, orderhistorie en track & trace opvragen."
  - name: orders.cancel
    description: "Een order annuleren."
  - name: orders.edit
    description: "Regels (variants) toevoegen aan een bestaande, nog niet verzonden order."
  - name: orders.shipping_address.update
    description: "Het verzendadres van een order wijzigen."
  - name: orders.notes.write
    description: "De interne notitie/opmerking van een order bijwerken."
  - name: refunds.create
    description: "Een refund voorbereiden (preview) en boeken, inclusief het opvragen van terugbetaalbare regels."
  - name: refunds.read
    description: "Reeds geboekte refunds van een order opvragen."
  - name: returns.create
    description: "Een retour (RMA) aanmaken en goedkeuren, inclusief het opvragen van retourneerbare regels."
  - name: returns.read
    description: "Retourstatus (RMA's) van een order opvragen."
  - name: customers.read
    description: "Klantgegevens en klant-orderhistorie opvragen, klant zoeken op telefoonnummer."
  - name: customers.write
    description: "Basisgegevens van een klant (e-mail, telefoon, naam) wijzigen."
  - name: customers.addresses.write
    description: "Een apart klantadres aanmaken, wijzigen of als standaard instellen."
  - name: customers.marketing_consent.write
    description: "De e-mailmarketingtoestemming van een klant wijzigen."
  - name: products.read
    description: "Producten zoeken, productdetails opvragen, voorraad opvragen, varianten/producten per ID opvragen."
  - name: products.metafields.read
    description: "Aangepaste productmetadata (custom fields) opvragen."
  - name: draft_orders.create
    description: "Een conceptorder (draft order) met betaallink aanmaken, bijvoorbeeld voor chat-checkout."
  - name: checkouts.read
    description: "Verlaten checkouts (abandoned checkouts) opvragen, voor hersteld-winkelwagen-flows."
  - name: knowledge.read
    description: "Beleidsdocumenten, pagina's en artikelen opvragen voor de kennisbank-sync."
  - name: catalog.sync
    description: "De volledige actieve productcatalogus itereren voor de RAG-index."
  - name: platform.info
    description: "Basisgegevens van de winkel opvragen; dient ook als verbindingstest."

security:
  - bearerAuth: []
  - apiKeyAuth: []
  - basicAuth: []
  - oauth2: []

paths:
  /.well-known/ragently:
    get:
      operationId: getManifest
      summary: Capability-manifest ophalen
      description: >-
        Wordt aangeroepen zodra een merchant zijn backend-URL en
        auth-gegevens invoert (en opnieuw bij elke "verbinding testen"), vóór
        elke andere aanroep. Geeft aan welke capabilities jouw backend
        ondersteunt. Ragently filtert onbekende capability-strings stilzwijgend
        weg en verbreedt de gedeclareerde set NOOIT zelf — je krijgt precies
        de endpoint-aanroepen die bij je opgegeven capabilities horen (plus,
        voor sommige tools, de intern impliceerde afhankelijkheden — zie de
        `Manifest`-schema-beschrijving).
      tags: [manifest]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
        - basicAuth: []
        - oauth2: []
      responses:
        "200":
          description: Manifest gevonden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Manifest"
        "4XX":
          description: "Authenticatie- of validatiefout."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /orders/find:
    get:
      operationId: findOrder
      summary: Order zoeken op vrije identifier
      description: >-
        Zoekt een order op GID/numeriek ID/"#naam"/vrije zoeknaam. Wordt
        gebruikt als eerste stap door vrijwel elke order-, refund- en
        return-tool (find_order resolvet de menselijke identifier naar een
        interne order-id vóórdat andere endpoints worden aangeroepen).
      tags: [orders.read]
      parameters:
        - name: identifier
          in: query
          required: true
          schema: { type: string }
          description: "Ordernummer, '#naam', GID, of vrije zoekterm."
      responses:
        "200":
          description: Order gevonden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderInfo"
        "404":
          description: "Geen order gevonden voor deze identifier (body wordt genegeerd)."

  /orders/{order_id}:
    get:
      operationId: getOrder
      summary: Order ophalen op ID (inclusief tracking)
      description: >-
        Pure by-id-variant van `find_order`; zelfde `OrderInfo`-shape. Wordt
        ook gebruikt voor de tracking-weergave (get_order_with_tracking) —
        dat is dezelfde aanroep, dus de tracking-informatie moet hier al in
        zitten als de order fulfillments heeft. Twee vormen worden
        geaccepteerd (zie `Fulfillment`-schema): de aanbevolen canonieke
        `trackingInfo[]`-array, of de platte legacy-velden
        (`trackingNumber`/`trackingCompany`/`trackingUrl`) — de connector
        normaliseert de platte vorm zelf naar canoniek, dus backends mogen
        vrij kiezen welke vorm ze leveren.
      tags: [orders.read]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
          description: "De interne order-id (zoals teruggegeven door find_order.id)."
      responses:
        "200":
          description: Order gevonden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderInfo"
        "404":
          description: "Order niet gevonden (body wordt genegeerd)."

  /orders:
    get:
      operationId: listOrdersByEmail
      summary: Alle orders van een klant op e-mailadres
      description: >-
        Levert de VOLLEDIGE (reeds samengevoegde) lijst orders van deze
        klant in één response — de custom-adapter doet hier zelf geen
        paginering/vervolgaanroepen op, dus je backend moet in één
        antwoord alle relevante orders teruggeven (eventueel intern beperkt
        door page_size als je zelf een limiet wilt communiceren).
      tags: [orders.read]
      parameters:
        - name: email
          in: query
          required: true
          schema: { type: string, format: email }
        - name: page_size
          in: query
          required: false
          schema: { type: integer, default: 50 }
          description: "Optionele hint voor het maximum aantal orders."
      responses:
        "200":
          description: "Lijst van orders (kan leeg zijn)."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/OrderInfo"

  /customers/{customer_id}/orders:
    get:
      operationId: getCustomerOrderHistory
      summary: Klant met diens recente orders
      description: "Klantnode met diens recente orders, gebruikt voor klant-order-historieweergaven."
      tags: [customers.read]
      parameters:
        - name: customer_id
          in: path
          required: true
          schema: { type: string }
        - name: first
          in: query
          required: false
          schema: { type: integer, default: 20 }
          description: "Maximumaantal orders om terug te geven."
      responses:
        "200":
          description: Klant met orders gevonden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerOrderHistory"
        "404":
          description: "Klant niet gevonden (body wordt genegeerd)."

  /orders/{order_id}/refunds:
    get:
      operationId: getOrderRefunds
      summary: Reeds geboekte refunds van een order
      tags: [refunds.read]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "Lijst van refunds (kan leeg zijn)."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RefundListItem"
        "404":
          description: "Order niet gevonden (body wordt genegeerd)."
    post:
      operationId: createRefund
      summary: Refund boeken
      tags: [refunds.create]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refund_line_items]
              properties:
                refund_line_items:
                  type: array
                  items:
                    $ref: "#/components/schemas/RefundLineItemInput"
                shipping_refund:
                  type: number
                  nullable: true
                notify_customer: { type: boolean, default: true }
                note:
                  type: string
                  nullable: true
      responses:
        "200":
          description: "Refund geboekt (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RefundResult"
        "404":
          description: "Order niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /orders/{order_id}/returns:
    get:
      operationId: getOrderReturns
      summary: Retouren (RMA's) van een order
      tags: [returns.read]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "Lijst van retouren (kan leeg zijn)."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ReturnListItem"
        "404":
          description: "Order niet gevonden (body wordt genegeerd)."
    post:
      operationId: createReturn
      summary: Retour (RMA) starten
      tags: [returns.create]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [return_line_items]
              properties:
                return_line_items:
                  type: array
                  items:
                    $ref: "#/components/schemas/ReturnLineItemInput"
                notify_customer: { type: boolean, default: true }
      responses:
        "200":
          description: "Retour aangemaakt (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReturnResult"
        "404":
          description: "Order niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /orders/{order_id}/refundable-items:
    get:
      operationId: getRefundableLineItems
      summary: Terugbetaalbare regels van een order
      description: >-
        Voedt de server-side naamresolutie van create_refund: de AI noemt
        producten bij naam, Ragently matcht dat tegen deze lijst om het
        juiste `id` (lineItemId) en de maximale `refundableQuantity` te
        bepalen. Sleutelnamen zijn bewust ongewijzigd t.o.v. de rauwe node.
      tags: [refunds.create]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "Lijst van terugbetaalbare regels (kan leeg zijn)."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RefundableLineItem"
        "404":
          description: "Order niet gevonden (body wordt genegeerd)."

  /orders/{order_id}/returnable-items:
    get:
      operationId: getReturnableLineItems
      summary: Retourneerbare regels van een order
      description: >-
        Voedt de server-side naamresolutie van create_return. LET OP: het
        `id`-veld leeft in een ANDERE id-ruimte dan
        `refundable-items[].id` — dit moet het id zijn dat create_return
        verwacht in `return_line_items[].fulfillmentLineItemId` (bij Shopify
        bijvoorbeeld een FulfillmentLineItem-GID, geen LineItem-GID).
        `quantity` is de RESTERENDE retourneerbare hoeveelheid (na aftrek van
        eerdere retouren); regels zonder resterende hoeveelheid horen niet in
        de lijst.
      tags: [returns.create]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "Lijst van retourneerbare regels (kan leeg zijn)."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ReturnableLineItem"
        "404":
          description: "Order niet gevonden (body wordt genegeerd)."

  /orders/{order_id}/notes:
    post:
      operationId: setOrderNote
      summary: Ordernotitie zetten/overschrijven
      tags: [orders.notes.write]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [note]
              properties:
                note: { type: string }
      responses:
        "200":
          description: "Notitie gezet (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SetOrderNoteResult"
        "404":
          description: "Order niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /orders/{order_id}/shipping-address:
    put:
      operationId: updateShippingAddress
      summary: Verzendadres van een order wijzigen
      tags: [orders.shipping_address.update]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address]
              properties:
                address:
                  $ref: "#/components/schemas/Address"
      responses:
        "200":
          description: "Adres gewijzigd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdateShippingAddressResult"
        "404":
          description: "Order niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /orders/{order_id}/cancel:
    post:
      operationId: cancelOrder
      summary: Order annuleren
      tags: [orders.cancel]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, default: customer }
                notify_customer: { type: boolean, default: true }
                restock: { type: boolean, default: true }
      responses:
        "200":
          description: "Order geannuleerd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancelOrderResult"
        "404":
          description: "Order niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /orders/{order_id}/line-items:
    post:
      operationId: orderEditAddVariant
      summary: Regel toevoegen aan een bestaande order
      description: >-
        Alleen bedoeld voor orders die nog volledig onverzonden zijn (de
        tool-laag van Ragently controleert dit al vóór de aanroep, maar je
        backend mag dit zelf ook afdwingen en met een foutresultaat
        antwoorden als de order al (deels) verzonden is).
      tags: [orders.edit]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [variant_id, quantity]
              properties:
                variant_id: { type: string }
                quantity: { type: integer, minimum: 1 }
                notify_customer: { type: boolean, default: true }
                staff_note:
                  type: string
                  nullable: true
      responses:
        "200":
          description: "Regel toegevoegd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderEditResult"
        "404":
          description: "Order niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /orders/{order_id}/refund/suggest:
    post:
      operationId: getSuggestedRefund
      summary: Refund-preview (zonder te boeken)
      description: >-
        Levert een preview van bedragen/transacties voor een voorgenomen
        refund, zonder iets te boeken. Wordt onder meer gebruikt om een
        geconfigureerd maximumbedrag (refund_max_amount) te toetsen vóórdat
        create_refund wordt aangeroepen.
      tags: [refunds.create]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refund_line_items]
              properties:
                refund_line_items:
                  type: array
                  items:
                    $ref: "#/components/schemas/RefundLineItemInput"
                shipping_amount:
                  type: number
                  nullable: true
      responses:
        "200":
          description: Refund-preview.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuggestedRefund"
        "404":
          description: "Order niet gevonden (body wordt genegeerd)."

  /returns/{return_id}/approve:
    post:
      operationId: approveReturn
      summary: Aangevraagde retour goedkeuren
      tags: [returns.create]
      parameters:
        - name: return_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "Retour goedgekeurd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApproveReturnResult"
        "404":
          description: "Retour niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /customers/{customer_id}:
    put:
      operationId: updateCustomer
      summary: Basisgegevens van een klant wijzigen
      tags: [customers.write]
      parameters:
        - name: customer_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email, nullable: true }
                phone: { type: string, nullable: true }
                first_name: { type: string, nullable: true }
                last_name: { type: string, nullable: true }
      responses:
        "200":
          description: "Klant gewijzigd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerUpdateResult"
        "404":
          description: "Klant niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /customers/by-phone:
    get:
      operationId: getCustomerByPhone
      summary: Klant zoeken op telefoonnummer
      description: "Retourneert uitsluitend een klant bij PRECIES ÉÉN match; bij nul of meerdere matches hoort dit endpoint 404 te geven."
      tags: [customers.read]
      parameters:
        - name: phone
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Eén klant gevonden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        "404":
          description: "Geen (of meer dan één) klant gevonden voor dit nummer (body wordt genegeerd)."

  /customers/{customer_id}/addresses:
    post:
      operationId: createCustomerAddress
      summary: Klantadres toevoegen
      tags: [customers.addresses.write]
      parameters:
        - name: customer_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address]
              properties:
                address:
                  $ref: "#/components/schemas/Address"
                set_as_default: { type: boolean, default: false }
      responses:
        "200":
          description: "Adres toegevoegd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerAddressResult"
        "404":
          description: "Klant niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /customers/{customer_id}/addresses/{address_id}:
    put:
      operationId: updateCustomerAddress
      summary: Bestaand klantadres wijzigen
      tags: [customers.addresses.write]
      parameters:
        - name: customer_id
          in: path
          required: true
          schema: { type: string }
        - name: address_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address]
              properties:
                address:
                  $ref: "#/components/schemas/Address"
                set_as_default: { type: boolean, default: false }
      responses:
        "200":
          description: "Adres gewijzigd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerAddressResult"
        "404":
          description: "Klant of adres niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /customers/{customer_id}/addresses/{address_id}/default:
    post:
      operationId: setDefaultCustomerAddress
      summary: Klantadres als standaard instellen
      tags: [customers.addresses.write]
      parameters:
        - name: customer_id
          in: path
          required: true
          schema: { type: string }
        - name: address_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "Adres als standaard ingesteld (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerAddressResult"
        "404":
          description: "Klant of adres niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /customers/{customer_id}/marketing-consent:
    put:
      operationId: updateEmailMarketingConsent
      summary: E-mailmarketingtoestemming wijzigen
      tags: [customers.marketing_consent.write]
      parameters:
        - name: customer_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [marketing_state]
              properties:
                marketing_state: { type: string }
                opt_in_level: { type: string, nullable: true }
      responses:
        "200":
          description: "Toestemming gewijzigd (of foutresultaat met success: false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketingConsentResult"
        "404":
          description: "Klant niet gevonden (body wordt genegeerd; de connector zet dit intern om naar {success: false, error: \"not_found\"})."

  /products/search:
    get:
      operationId: searchProducts
      summary: Producten zoeken (met optionele details/voorraad)
      description: >-
        Eén en hetzelfde endpoint bedient VIER interne aanroepvormen van de
        connector, onderscheiden door de query-parameters:

        - Zonder `details`/`stock`: vrije productzoekopdracht
          (search_products) — items volgens `ProductSearchItem`.
        - `details=1`: rijke productdetails (get_product_details) — items
          volgens `ProductDetailItem`.
        - `stock=1`: voorraadoverzicht (get_product_stock) — items volgens
          `ProductStockItem`.
        - `limit=1` zonder verdere vlaggen: ook gebruikt om één product-ID
          te resolven (resolve_product_id) uit `results[0].product_id`.

        Implementeer minimaal de basisvorm (`ProductSearchItem`, inclusief
        `product_id`); `details`/`stock` zijn alleen nodig als je ook de
        bijbehorende PRODUCTS_READ-subfuncties wilt ondersteunen (deze delen
        dezelfde capability, dus worden altijd samen aangeboden zodra
        products.read gedeclareerd is).
      tags: [products.read]
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          description: "Vrije zoekterm (mag leeg zijn, bv. bij list-achtig gebruik)."
        - name: limit
          in: query
          required: false
          schema: { type: integer, default: 5 }
        - name: details
          in: query
          required: false
          schema: { type: string, enum: ["1"] }
          description: "Indien '1': retourneer ProductDetailItem-vorm."
        - name: stock
          in: query
          required: false
          schema: { type: string, enum: ["1"] }
          description: "Indien '1': retourneer ProductStockItem-vorm."
      responses:
        "200":
          description: "Zoekresultaten (kan leeg zijn)."
          content:
            application/json:
              schema:
                type: array
                items:
                  oneOf:
                    - $ref: "#/components/schemas/ProductSearchItem"
                    - $ref: "#/components/schemas/ProductDetailItem"
                    - $ref: "#/components/schemas/ProductStockItem"

  /products/variants:
    post:
      operationId: getVariantsByIds
      summary: Varianten opvragen per ID
      tags: [products.read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [variant_ids]
              properties:
                variant_ids:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: "Dict per variant-id (kan leeg zijn)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VariantsByIdsResult"

  /products/by-ids:
    post:
      operationId: getProductsByIds
      summary: Producten opvragen per ID
      tags: [products.read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [product_ids]
              properties:
                product_ids:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: "Dict per product-id (kan leeg zijn)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductsByIdsResult"

  /products/{product_id}/metafields:
    get:
      operationId: getProductMetafields
      summary: Productmetafields opvragen
      tags: [products.metafields.read]
      parameters:
        - name: product_id
          in: path
          required: true
          schema: { type: string }
        - name: namespace
          in: query
          required: false
          schema: { type: string, nullable: true }
        - name: first
          in: query
          required: false
          schema: { type: integer, default: 50 }
      responses:
        "200":
          description: Metafields gevonden.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductMetafields"
        "404":
          description: "Product niet gevonden (body wordt genegeerd)."

  /draft-orders:
    post:
      operationId: createDraftOrder
      summary: Conceptorder met betaallink aanmaken
      tags: [draft_orders.create]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [line_items]
              properties:
                line_items:
                  type: array
                  items:
                    $ref: "#/components/schemas/DraftOrderLineItemInput"
                email: { type: string, format: email, nullable: true }
                note: { type: string, nullable: true }
                note_attributes:
                  type: array
                  nullable: true
                  items:
                    type: object
                    properties:
                      name: { type: string }
                      value: { type: string }
                order_discount:
                  type: object
                  nullable: true
                  additionalProperties: true
                  description: "Vorm is platformafhankelijk; laat leeg als je geen orderkorting ondersteunt."
                free_shipping: { type: boolean, default: false }
                tags:
                  type: array
                  nullable: true
                  items: { type: string }
      responses:
        "200":
          description: Conceptorder aangemaakt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DraftOrderResult"

  /abandoned-checkouts:
    get:
      operationId: listAbandonedCheckouts
      summary: Verlaten checkouts opvragen (gepagineerd)
      description: >-
        Wordt herhaald aangeroepen met oplopende `page` totdat je een lege
        lijst teruggeeft (of totdat de connector eerder stopt op basis van
        eigen stopcriteria). Geef in elk item `customer.email` mee — het
        e-mailfilter (matchen tegen de ingelogde klant) gebeurt aan
        Ragently's kant, niet in dit contract.
      tags: [checkouts.read]
      parameters:
        - name: page_size
          in: query
          required: false
          schema: { type: integer, default: 50 }
        - name: page
          in: query
          required: false
          schema: { type: integer, default: 1 }
          description: "1-based paginanummer."
      responses:
        "200":
          description: "Pagina met verlaten checkouts; lege array betekent einde van de paginering."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AbandonedCheckout"

  /catalog/products:
    get:
      operationId: iterCatalogProducts
      summary: Volledige productcatalogus itereren (gepagineerd)
      description: >-
        Wordt herhaald aangeroepen met oplopende `page` (startend bij 1)
        totdat je een lege lijst teruggeeft — dat is het signaal dat de
        catalogus-sync klaar is. Gebruikt voor de RAG-productindex.
      tags: [catalog.sync]
      parameters:
        - name: page
          in: query
          required: true
          schema: { type: integer, default: 1 }
          description: "1-based paginanummer."
      responses:
        "200":
          description: "Pagina met producten; lege array betekent einde van de paginering."
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CatalogProduct"

  /knowledge:
    get:
      operationId: fetchKnowledgeDocuments
      summary: Shopcontent voor de kennisbank
      tags: [knowledge.read]
      responses:
        "200":
          description: Kennisbankdocumenten.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeDocuments"

  /shop:
    get:
      operationId: getShopInfo
      summary: Basisgegevens van de winkel (verbindingstest)
      description: >-
        Dit is (indien gedeclareerd) het EERSTE endpoint dat Ragently
        aanroept om een nieuwe koppeling te testen — implementeer dit als
        een lichte, snelle, altijd-beschikbare read als je platform.info
        declareert.
      tags: [platform.info]
      responses:
        "200":
          description: Winkelgegevens.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShopInfo"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Auth-methode 'bearer': Ragently stuurt 'Authorization: Bearer <token>' mee op elk verzoek."
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        Auth-methode 'api_key': Ragently stuurt jouw API-key mee in een
        header. Standaard heet die header 'X-API-Key'; bij het koppelen kun
        je een andere headernaam opgeven (header_name), die wordt dan
        gebruikt in plaats van X-API-Key.
    basicAuth:
      type: http
      scheme: basic
      description: "Auth-methode 'basic': Ragently stuurt HTTP Basic Authentication mee (username/password)."
    oauth2:
      type: oauth2
      description: >-
        Auth-methode 'oauth2': Ragently haalt zelf een access-token op via
        client-credentials-grant bij jouw token_url en stuurt die vervolgens
        mee als 'Authorization: Bearer <access_token>'. Token wordt gecachet
        en op tijd ververst (60s marge vóór expiry).
      flows:
        clientCredentials:
          tokenUrl: "https://your-backend.example.com/oauth/token"
          scopes: {}

  schemas:
    Manifest:
      type: object
      description: >-
        Response van GET /.well-known/ragently. `capabilities` bepaalt
        exact welke van de overige endpoints in dit document Ragently zal
        aanroepen: declareer alleen wat je ook echt implementeert.
        Sommige tools resolven intern via een AFHANKELIJKE capability (bv.
        orders.cancel/orders.edit/refunds.create/refunds.read/returns.create/
        returns.read/orders.shipping_address.update/orders.notes.write
        impliceren allemaal ook orders.read, omdat ze eerst find_order
        aanroepen; customers.write/customers.addresses.write/
        customers.marketing_consent.write impliceren customers.read;
        products.metafields.read impliceert products.read;
        draft_orders.create impliceert
        products.read voor variant-naamresolutie) — deze afhankelijkheden
        hoef je niet apart te declareren, maar je moet de onderliggende
        endpoints (bv. GET /orders/find) WEL implementeren als je een
        afhankelijke capability declareert.
      required: [version, capabilities]
      properties:
        version:
          type: string
          description: "Vrije versie-string van jouw manifest/implementatie."
        capabilities:
          type: array
          items:
            type: string
            enum:
              - orders.read
              - orders.cancel
              - orders.edit
              - orders.shipping_address.update
              - refunds.create
              - refunds.read
              - returns.create
              - returns.read
              - customers.read
              - customers.write
              - orders.notes.write
              - customers.addresses.write
              - products.read
              - products.metafields.read
              - draft_orders.create
              - checkouts.read
              - customers.marketing_consent.write
              - knowledge.read
              - catalog.sync
              - platform.info

    Error:
      type: object
      description: "Aanbevolen foutvorm voor niet-2xx-responses (behalve 404, waarvan de body genegeerd wordt)."
      properties:
        error:
          type: string

    Address:
      type: object
      description: "Adresvorm zoals gebruikt in shipping-address- en customer-address-endpoints."
      properties:
        address1: { type: string }
        address2: { type: string }
        city: { type: string }
        province: { type: string }
        zip: { type: string }
        country: { type: string }
        phone: { type: string }
      additionalProperties: true

    Customer:
      type: object
      description: "Klantobject; base.py laat de exacte binnenkant grotendeels los — dit zijn de velden die de connector daadwerkelijk leest/schrijft."
      properties:
        id: { type: string }
        email: { type: string, format: email, nullable: true }
        phone: { type: string, nullable: true }
        first_name: { type: string, nullable: true }
        last_name: { type: string, nullable: true }
      additionalProperties: true

    LineItem:
      type: object
      description: "Regel binnen een order; het contract laat de precieze veldset los buiten wat elders expliciet gedocumenteerd is (RefundableLineItem/ReturnableLineItem hebben hun eigen, striktere vorm)."
      properties:
        id: { type: string }
        name: { type: string }
        quantity: { type: integer }
      additionalProperties: true

    Fulfillment:
      type: object
      description: >-
        Fulfillment inclusief tracking-informatie; vorm is platformafhankelijk
        (base.py legt hier geen strikte velden op), vandaar
        additionalProperties. Twee manieren om tracking aan te leveren — de
        connector normaliseert de platte legacy-velden zelf naar de canonieke
        vorm, dus lever ÉÉN van beide, niet allebei (canoniek heeft voorrang
        als het aanwezig is). Aanbevolen is de canonieke `trackingInfo[]`-
        array: één item per trackingnummer, platformonafhankelijk, geschikt
        voor multi-colli-fulfillments. De platte legacy-velden
        `trackingNumber`/`trackingCompany`/`trackingUrl` (max. één
        trackingnummer per fulfillment) blijven ondersteund voor bestaande
        integraties.
      properties:
        id: { type: string }
        status: { type: string }
        trackingInfo:
          type: array
          description: "Aanbevolen, canonieke vorm. Eén entry per trackingnummer."
          items:
            $ref: "#/components/schemas/FulfillmentTrackingInfo"
        trackingCompany:
          type: string
          nullable: true
          description: "Legacy, blijft ondersteund. Wordt genormaliseerd naar trackingInfo[0].company als trackingInfo ontbreekt."
        trackingNumber:
          type: string
          nullable: true
          description: "Legacy, blijft ondersteund. Wordt genormaliseerd naar trackingInfo[0].number als trackingInfo ontbreekt."
        trackingUrl:
          type: string
          nullable: true
          description: "Legacy, blijft ondersteund. Wordt genormaliseerd naar trackingInfo[0].url als trackingInfo ontbreekt."
        latest_event:
          allOf:
            - $ref: "#/components/schemas/FulfillmentLatestEvent"
          nullable: true
          description: "Optioneel: meest recente track&trace-gebeurtenis, indien de backend die bijhoudt."
        estimated_delivery:
          type: string
          format: date-time
          nullable: true
          description: "Optioneel: geschatte afleverdatum/-tijd, indien bekend bij de backend."
      additionalProperties: true

    FulfillmentTrackingInfo:
      type: object
      description: "Canoniek trackingitem — zelfde vorm als Shopify fulfillments[].trackingInfo[]."
      required:
        - number
      properties:
        number: { type: string, description: "Trackingnummer." }
        url: { type: string, nullable: true, description: "Track&trace-URL van de vervoerder." }
        company: { type: string, nullable: true, description: "Naam van de vervoerder (bv. 'postnl', 'DHL', 'GLS')." }
      additionalProperties: false

    FulfillmentLatestEvent:
      type: object
      description: "Meest recente track&trace-gebeurtenis van een fulfillment."
      required:
        - status
      properties:
        status: { type: string, description: "Status van de gebeurtenis (vrije, vervoerder-specifieke tekst of genormaliseerde waarde)." }
        happened_at: { type: string, format: date-time, nullable: true }
        description: { type: string, nullable: true, description: "Mens-leesbare toelichting bij de gebeurtenis." }
      additionalProperties: false

    OrderInfo:
      type: object
      description: >-
        Canonieke order-shape (base.py: find_order/get_order/
        get_order_with_tracking/list_orders_by_email). Platformonafhankelijk
        — dezelfde vorm ongeacht welk order-endpoint hem teruggeeft.
      required:
        - id
        - name
        - displayFinancialStatus
        - displayFulfillmentStatus
        - createdAt
      properties:
        id: { type: string }
        legacyResourceId: { type: string, nullable: true }
        name: { type: string, description: "Mens-leesbaar ordernummer, bv. '#1001'." }
        email: { type: string, format: email, nullable: true }
        phone: { type: string, nullable: true }
        displayFinancialStatus: { type: string }
        displayFulfillmentStatus: { type: string }
        createdAt: { type: string, format: date-time }
        customer:
          allOf:
            - $ref: "#/components/schemas/Customer"
          nullable: true
        shippingAddress:
          allOf:
            - $ref: "#/components/schemas/Address"
          nullable: true
        fulfillments:
          type: array
          items:
            $ref: "#/components/schemas/Fulfillment"
        lineItems:
          type: array
          items:
            $ref: "#/components/schemas/LineItem"
      additionalProperties: true

    CustomerOrderHistory:
      type: object
      description: "Klantnode met diens recente orders. Vorm is losjes gedocumenteerd in base.py; verwacht wordt minstens een lijst orders (bv. onder 'orders')."
      additionalProperties: true

    RefundLineItemSummary:
      type: object
      properties:
        name: { type: string }
        quantity: { type: integer }
      additionalProperties: true

    RefundListItem:
      type: object
      description: "Item in GET /orders/{order_id}/refunds."
      required: [refund_id, amount, currency]
      properties:
        refund_id: { type: string }
        created_at: { type: string, format: date-time, nullable: true }
        amount: { type: number }
        currency: { type: string }
        note: { type: string, nullable: true }
        items:
          type: array
          items:
            $ref: "#/components/schemas/RefundLineItemSummary"

    ReturnLineItemSummary:
      type: object
      properties:
        name: { type: string }
        quantity: { type: integer }
      additionalProperties: true

    ReturnListItem:
      type: object
      description: "Item in GET /orders/{order_id}/returns."
      required: [return_id, status]
      properties:
        return_id: { type: string }
        status: { type: string }
        created_at: { type: string, format: date-time, nullable: true }
        items:
          type: array
          items:
            $ref: "#/components/schemas/ReturnLineItemSummary"

    RefundableLineItem:
      type: object
      description: "Item in GET /orders/{order_id}/refundable-items. Sleutelnamen bewust ongewijzigd t.o.v. de rauwe node (tools lezen die keys direct)."
      required: [id, name, quantity, refundableQuantity]
      properties:
        id: { type: string, description: "LineItem-id; dit is het id dat create_refund verwacht in refund_line_items[].lineItemId." }
        name: { type: string }
        quantity: { type: integer }
        refundableQuantity: { type: integer }

    ReturnableLineItem:
      type: object
      description: "Item in GET /orders/{order_id}/returnable-items. 'id' leeft in een ANDERE id-ruimte dan RefundableLineItem.id — dit is het id dat create_return verwacht in return_line_items[].fulfillmentLineItemId."
      required: [id, name, quantity]
      properties:
        id: { type: string }
        name: { type: string }
        quantity:
          type: integer
          description: "RESTERENDE retourneerbare hoeveelheid, na aftrek van eerdere retouren."

    RefundLineItemInput:
      type: object
      description: "Regel zoals doorgegeven aan refund/suggest en refunds (create_refund)."
      required: [lineItemId, quantity]
      properties:
        lineItemId: { type: string }
        quantity: { type: integer, minimum: 1 }
        restockType:
          type: string
          nullable: true
          description: "Bv. 'NO_RESTOCK'; platformafhankelijk, mag genegeerd worden als niet van toepassing."

    ReturnLineItemInput:
      type: object
      description: "Regel zoals doorgegeven aan create_return."
      required: [fulfillmentLineItemId, quantity]
      properties:
        fulfillmentLineItemId: { type: string }
        quantity: { type: integer, minimum: 1 }

    SuggestedRefund:
      type: object
      description: >-
        Preview van bedragen/transacties voor een refund. base.py laat de
        exacte binnenkant los; de connector leest in elk geval
        `suggestedTransactions[].amount` (som daarvan) als het
        refund_max_amount-maximum getoetst wordt.
      properties:
        suggestedTransactions:
          type: array
          items:
            type: object
            properties:
              amount: { type: number }
            additionalProperties: true
      additionalProperties: true

    RefundResult:
      type: object
      description: "Response van POST /orders/{order_id}/refunds."
      required: [success]
      properties:
        success: { type: boolean }
        refund_id: { type: string, nullable: true }
        amount: { type: number, nullable: true }
        currency: { type: string, nullable: true }
        error: { type: string, nullable: true }
      additionalProperties: true

    ReturnResult:
      type: object
      description: "Response van POST /orders/{order_id}/returns. Exacte binnenkant bij success is losjes gedocumenteerd; success/error is het enige harde contract."
      required: [success]
      properties:
        success: { type: boolean }
        error: { type: string, nullable: true }
      additionalProperties: true

    ApproveReturnResult:
      type: object
      description: "Response van POST /returns/{return_id}/approve."
      required: [success]
      properties:
        success: { type: boolean }
        error: { type: string, nullable: true }
      additionalProperties: true

    SetOrderNoteResult:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        note: { type: string, nullable: true }
        errors:
          type: array
          items: { type: string }
        error: { type: string, nullable: true }

    UpdateShippingAddressResult:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        order:
          allOf:
            - $ref: "#/components/schemas/OrderInfo"
          nullable: true
        error: { type: string, nullable: true }

    CancelOrderResult:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        job:
          type: object
          nullable: true
          additionalProperties: true
          description: "Platformafhankelijk (bv. een async-annuleerjob-referentie); laat weg of vul in wat past."
        error: { type: string, nullable: true }

    OrderEditResult:
      type: object
      description: "Response van POST /orders/{order_id}/line-items. base.py specificeert geen velden verder dan het algemene schrijf-resultaatpatroon (success/error); vul aan wat relevant is."
      required: [success]
      properties:
        success: { type: boolean }
        error: { type: string, nullable: true }
      additionalProperties: true

    CustomerUpdateResult:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        customer:
          allOf:
            - $ref: "#/components/schemas/Customer"
          nullable: true
        error: { type: string, nullable: true }

    CustomerAddressResult:
      type: object
      description: "Gedeeld resultaatschema voor create/update/set-default van klantadressen. base.py documenteert hier geen exacte velden buiten het algemene success-patroon."
      required: [success]
      properties:
        success: { type: boolean }
        address:
          allOf:
            - $ref: "#/components/schemas/Address"
          nullable: true
        error: { type: string, nullable: true }
      additionalProperties: true

    MarketingConsentResult:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        marketing_state: { type: string, nullable: true }
        errors:
          type: array
          items: { type: string }
        error: { type: string, nullable: true }

    ProductSearchItem:
      type: object
      description: "Standaardvorm van GET /products/search (search_products / resolve_product_id)."
      properties:
        product_id:
          type: string
          nullable: true
          description: "Niet expliciet genoemd in de CommerceClient-abstractie, maar wel gelezen door resolve_product_id (results[0].product_id) — vereist als je resolve_product_id/PRODUCTS_READ-naamresolutie wilt ondersteunen."
        product_title: { type: string }
        variant_title: { type: string, nullable: true }
        variant_id: { type: string, nullable: true }
        variantId: { type: string, nullable: true, description: "Alias van variant_id; sommige toolpaden lezen deze key." }
        price: { type: number, nullable: true }
        available: { type: boolean, nullable: true }
        image_url: { type: string, nullable: true }
      additionalProperties: true

    ProductDetailVariant:
      type: object
      properties:
        title: { type: string }
        price: { type: number, nullable: true }
        sku: { type: string, nullable: true }
        available: { type: boolean, nullable: true }
        stock: { type: integer, nullable: true }
      additionalProperties: true

    ProductDetailItem:
      type: object
      description: "Vorm van GET /products/search?details=1 (get_product_details)."
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        product_type: { type: string, nullable: true }
        vendor: { type: string, nullable: true }
        tags:
          type: array
          items: { type: string }
        variants:
          type: array
          items:
            $ref: "#/components/schemas/ProductDetailVariant"
      additionalProperties: true

    ProductStockVariant:
      type: object
      properties:
        variant: { type: string }
        available: { type: boolean, nullable: true }
        stock: { type: integer, nullable: true }
        continues_selling_when_out_of_stock: { type: boolean, nullable: true }
      additionalProperties: true

    ProductStockItem:
      type: object
      description: "Vorm van GET /products/search?stock=1 (get_product_stock)."
      properties:
        title: { type: string }
        variants:
          type: array
          items:
            $ref: "#/components/schemas/ProductStockVariant"
      additionalProperties: true

    VariantInfo:
      type: object
      description: "Waarde-object in VariantsByIdsResult."
      properties:
        variant_title: { type: string }
        price: { type: number, nullable: true }
        available: { type: boolean, nullable: true }
        image_url: { type: string, nullable: true }
      additionalProperties: true

    VariantsByIdsResult:
      type: object
      description: "Response van POST /products/variants: dict per variant-id -> VariantInfo."
      additionalProperties:
        $ref: "#/components/schemas/VariantInfo"

    ProductByIdInfo:
      type: object
      description: "Waarde-object in ProductsByIdsResult. base.py laat de exacte 'varianten...'-inhoud los buiten title/handle/image_url."
      properties:
        title: { type: string }
        handle: { type: string, nullable: true }
        image_url: { type: string, nullable: true }
      additionalProperties: true

    ProductsByIdsResult:
      type: object
      description: "Response van POST /products/by-ids: dict per product-id -> ProductByIdInfo."
      additionalProperties:
        $ref: "#/components/schemas/ProductByIdInfo"

    Metafield:
      type: object
      required: [namespace, key, type, value]
      properties:
        namespace: { type: string }
        key: { type: string }
        type: { type: string }
        value: { type: string }

    ProductMetafields:
      type: object
      description: "Response van GET /products/{product_id}/metafields."
      required: [metafields]
      properties:
        product_title: { type: string, nullable: true }
        handle: { type: string, nullable: true }
        metafields:
          type: array
          items:
            $ref: "#/components/schemas/Metafield"

    DraftOrderLineItemInput:
      type: object
      description: "Regel zoals doorgegeven aan create_draft_order; variantId is server-side al geresolved uit de door de AI genoemde producttitel."
      required: [variantId, quantity]
      properties:
        variantId: { type: string }
        quantity: { type: integer, minimum: 1 }

    DraftOrderResult:
      type: object
      description: "Response van POST /draft-orders."
      required: [draft_order_id, invoice_url]
      properties:
        draft_order_id: { type: string }
        invoice_url: { type: string, description: "Betaallink die aan de klant getoond wordt." }
        total_price: { type: number, nullable: true }
        currency: { type: string, nullable: true }
        status: { type: string, nullable: true }
      additionalProperties: true

    AbandonedCheckout:
      type: object
      description: >-
        Rauwe abandoned-checkout-node. base.py legt hier geen strikt schema
        op behalve dat 'customer.email' aanwezig moet zijn (het
        e-mailfilter tegen de ingelogde klant gebeurt bij Ragently).
      required: [customer]
      properties:
        id: { type: string, nullable: true }
        customer:
          type: object
          required: [email]
          properties:
            email: { type: string, format: email }
          additionalProperties: true
      additionalProperties: true

    CatalogProduct:
      type: object
      description: "Genormaliseerd, platformonafhankelijk catalogusitem (item in GET /catalog/products), gebruikt voor de RAG-productindex."
      required: [product_id, title]
      properties:
        product_id: { type: string }
        title: { type: string }
        description: { type: string, description: "Plain text (geen HTML)." }
        tags:
          type: array
          items: { type: string }
        product_type: { type: string, nullable: true }
        vendor: { type: string, nullable: true }
        handle: { type: string, nullable: true }
        image_url: { type: string, nullable: true }
        variant_ids:
          type: array
          items: { type: string }

    KnowledgeArticleLike:
      type: object
      description: "Gedeeld itemschema voor policies/pages/articles."
      required: [title, body]
      properties:
        title: { type: string }
        body: { type: string, description: "HTML-inhoud." }

    KnowledgeDocuments:
      type: object
      description: >-
        Response van GET /knowledge. Alleen de 'kinds' die jouw platform
        daadwerkelijk heeft hoeven aanwezig te zijn; 'errors' is optioneel en
        bevat per ontbrekende/mislukte kind een foutmelding.
      properties:
        policies:
          type: array
          items:
            $ref: "#/components/schemas/KnowledgeArticleLike"
        pages:
          type: array
          items:
            $ref: "#/components/schemas/KnowledgeArticleLike"
        articles:
          type: array
          items:
            $ref: "#/components/schemas/KnowledgeArticleLike"
        errors:
          type: object
          additionalProperties:
            type: string
          description: "Map van kind (bv. 'policies') naar foutmelding, alleen voor kinds die mislukten."

    ShopInfo:
      type: object
      description: "Response van GET /shop."
      required: [id, name]
      properties:
        id: { type: string }
        name: { type: string }
        email: { type: string, format: email, nullable: true }
        domain: { type: string, nullable: true }
        plan_name: { type: string, nullable: true }
      additionalProperties: true
