openapi: 3.1.0
info:
  title: Venty ERP API — Administração completa (catálogo, estoque multi-local, clientes, pedidos)
  version: "2.0.0"
  x-audience: [erp-integrators, ai-agents]
  description: |
    API máquina-a-máquina para um ERP **controlar 100% das ações administrativas** de
    uma loja Venty: produtos (todos os campos), estoque **por localização** (multi-local),
    localizações (lojas/depósitos), transferências (crossdock), categorias, marcas, grupos,
    atributos e variações, clientes (cadastro PF/PJ) e pedidos (status/fulfillment).

    ## Regras de ouro (valem em TODO endpoint — redundante de propósito)
    1. **Autentique** em `POST /api/v2/oauth/token` (client_credentials) → recebe um
       `access_token` Bearer (~10 min).
    2. **Envie** `Authorization: Bearer <access_token>` em toda chamada.
    3. **O tenant (a loja) vem do TOKEN** — nunca do corpo nem da URL. Não há `tenant_id`
       nos paths v2.
    4. **Escopos** por recurso (o dono da loja concede à chave):
       `catalog:read/write` (produtos + toda a taxonomia + mídia + cross-sell),
       `inventory:read/write` (estoque + localizações + malha + transferências),
       `customers:read/write`, `orders:read/write`, `reports:read`, `fiscal:read`.
    5. **Estoque é SET absoluto** (não delta): entrada/baixa = read-modify-write (GET → PUT).
    6. **Ambiente pela CHAVE:** `vk_test_…` = homologação (`https://api.hml.venty.me`),
       `vk_live_…` = produção (`https://api.venty.me`).

    ## Estoque multi-local (o essencial)
    Cada produto tem `stock_mode`: `global` (um número) ou `per_location` (por
    **localização** = uma `location` = loja/depósito). Uma localização é criada em
    `/api/v2/inventory/locations` e o seu `id` é o `origin_id` usado nos níveis
    (`levels[].origin_id`). Disponível per_location = Σ(`on_hand - reserved`) entre
    localizações. Não é possível baixar abaixo do `reserved` → 409.

    ## Produto variável (fluxo resumido)
    (1) crie `variation-groups` (eixos: Cor, Tamanho) e `variation-terms` (valores);
    (2) crie os `attributes`/`attribute-values` (a identidade da diferenciação);
    (3) crie o produto `parent` (sem preço); (4) crie cada `variation` com
    `parent_product_id`, `price`, `variation_attributes` (group→term) e
    `product_attributes` (role=`differentiation`). Ver o fluxo completo em docs.venty.me.

servers:
  - url: https://api.venty.me
    description: Produção — chaves vk_live_.
  - url: https://api.hml.venty.me
    description: Homologação/teste — chaves vk_test_.

security:
  - OAuth2ClientCredentials: []

tags:
  - { name: Autenticação, description: Token de máquina (OAuth2 client_credentials). }
  - { name: Produtos, description: Material master — CRUD com todos os campos, variações. }
  - { name: Estoque, description: Níveis de estoque (global e por localização). }
  - { name: Localizações, description: Lojas/depósitos — a base do multi-local. }
  - { name: Transferências, description: Malha + crossdock (mover estoque entre localizações). }
  - { name: Categorias, description: Árvore de categorias. }
  - { name: Marcas, description: Marcas (fabricantes). }
  - { name: Grupos de produto, description: Famílias + eixos de variação. }
  - { name: Atributos, description: Atributos + valores (identidade de diferenciação). }
  - { name: Variações, description: Grupos + termos de variação (vitrine). }
  - { name: Mídia, description: Galeria (imagens/vídeo) por produto. }
  - { name: Venda-casada, description: Pools de cross-sell. }
  - { name: Clientes, description: Cadastro de clientes PF/PJ. }
  - { name: Pedidos, description: Leitura + status/fulfillment. }
  - { name: Relatórios, description: Curva ABC, vendas. }

paths:
  # ─────────────────────────── AUTENTICAÇÃO ───────────────────────────
  /api/v2/oauth/token:
    post:
      tags: [Autenticação]
      operationId: issueToken
      summary: Emitir token de máquina
      security: []
      description: |
        Troca `client_id` + `client_secret` por um `access_token` Bearer (~10 min).
        Aceita form-urlencoded (padrão OAuth) ou JSON. Escopos efetivos vêm do grant da chave.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/TokenRequest' }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenResponse' } } } }
        '401': { description: client inválido, content: { application/json: { schema: { $ref: '#/components/schemas/OAuthError' } } } }

  # ─────────────────────────── PRODUTOS ───────────────────────────
  /api/v2/catalog/products:
    get:
      tags: [Produtos]
      operationId: listProducts
      summary: Listar produtos (cursor)
      description: 'Requer `catalog:read`. Paginação por cursor opaco.'
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 250 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Product' } }, next_cursor: { type: [string, "null"] } } } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Produtos]
      operationId: createProduct
      summary: Criar produto (todos os campos)
      description: 'Requer `catalog:write`. Ver `ProductWrite` para o conjunto COMPLETO de campos (dimensões, custo, imposto, fiscal, categoria/marca, variação, custom_fields).'
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProductWrite' }, examples: { simples: { value: { name: "Placa ST 13", slug: "placa-st-13", sku: "PLST13", price: 18.03, stock: 120, weight: 12.5, cost_price: 11.20, category_id: "…", brand_id: "…" } } } } } }
      responses:
        '201': { description: Criado, content: { application/json: { schema: { type: object, properties: { id: { type: string, format: uuid } } } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { description: slug/sku duplicado ou variação duplicada }
  /api/v2/catalog/products/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    get:
      tags: [Produtos]
      operationId: getProduct
      summary: Ler produto
      description: 'Requer `catalog:read`.'
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Product' } } } }, '401': { $ref: '#/components/responses/Unauthorized' }, '404': { $ref: '#/components/responses/NotFound' } }
    put:
      tags: [Produtos]
      operationId: updateProduct
      summary: Atualizar produto (parcial)
      description: 'Requer `catalog:write`. Ver notas de preservação vs. limpeza em `ProductWrite`.'
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProductWrite' } } } }
      responses: { '200': { $ref: '#/components/responses/Ok' }, '401': { $ref: '#/components/responses/Unauthorized' }, '404': { $ref: '#/components/responses/NotFound' } }

  # ─────────────────────────── ESTOQUE ───────────────────────────
  /api/v2/catalog/products/{id}/inventory:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    get:
      tags: [Estoque]
      operationId: getInventory
      summary: Ler estoque (modo + níveis por localização)
      description: 'Requer `catalog:read`. Uma linha por localização ativa. Passo 1 do in/out.'
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Inventory' } } } }, '401': { $ref: '#/components/responses/Unauthorized' }, '404': { $ref: '#/components/responses/NotFound' } }
    put:
      tags: [Estoque]
      operationId: setInventory
      summary: Definir estoque (entrada/saída — SET absoluto)
      description: |
        Requer `inventory:write`. Global → `{"stock":N}`. Multi-local →
        `{"levels":[{"origin_id":"<location id>","on_hand":N}]}`. Opcional `stock_mode`.
        Baixar abaixo do reservado → 409 `estoque_abaixo_do_reservado`.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryWrite' } } } }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Inventory' } } } }, '401': { $ref: '#/components/responses/Unauthorized' }, '409': { description: abaixo do reservado } }

  # ─────────────────────────── LOCALIZAÇÕES ───────────────────────────
  /api/v2/inventory/locations:
    get:
      tags: [Localizações]
      operationId: listLocations
      summary: Listar localizações (lojas/depósitos)
      description: 'Requer `inventory:read`. O `id` de cada localização é o `origin_id` dos níveis de estoque.'
      responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { origins: { type: array, items: { $ref: '#/components/schemas/Location' } } } } } } }, '401': { $ref: '#/components/responses/Unauthorized' } }
    post:
      tags: [Localizações]
      operationId: createLocation
      summary: Criar localização
      description: 'Requer `inventory:write`. Geocoda o CEP. `type=warehouse` força só-transportadora (sem pickup/frete próprio).'
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/LocationWrite' }, examples: { loja: { value: { name: "Loja Centro", type: "store", cep: "20221030", address: "Rua X", number: "3", city: "Rio de Janeiro", state: "RJ", pickup_enabled: true, pickup_ready_hours: 24 } } } } } }
      responses: { '201': { description: Criado, content: { application/json: { schema: { type: object, properties: { id: { type: string, format: uuid }, geocoded: { type: boolean } } } } } }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /api/v2/inventory/locations/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put:
      tags: [Localizações]
      operationId: updateLocation
      summary: Atualizar localização
      description: 'Requer `inventory:write` (full-replace — reenvie todos os campos).'
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/LocationWrite' } } } }
      responses: { '200': { $ref: '#/components/responses/Ok' }, '404': { $ref: '#/components/responses/NotFound' } }
    delete:
      tags: [Localizações]
      operationId: deleteLocation
      summary: Remover localização
      description: 'Requer `inventory:write`. Hard delete — garanta que não há níveis/transferências dependentes.'
      responses: { '200': { $ref: '#/components/responses/Ok' }, '404': { $ref: '#/components/responses/NotFound' } }

  # ─────────────────────────── TRANSFERÊNCIAS ───────────────────────────
  /api/v2/inventory/transfer-routes:
    get: { tags: [Transferências], operationId: listRoutes, summary: Listar malha de transferência, description: 'Requer `inventory:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Transferências], operationId: upsertRoute, summary: Criar/atualizar rota (from→to), description: 'Requer `inventory:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TransferRouteWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Ok' } } }
  /api/v2/inventory/transfer-routes/suggest:
    post: { tags: [Transferências], operationId: suggestRoutes, summary: Auto-sugerir rotas por proximidade, description: 'Requer `inventory:write`. Propõe rotas (desabilitadas) entre localizações geocodadas ≤300km.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/inventory/transfer-routes/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Transferências], operationId: updateRoute, summary: Atualizar rota (prazo/enabled), description: 'Requer `inventory:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TransferRouteWrite' } } } }, responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Transferências], operationId: deleteRoute, summary: Remover rota, description: 'Requer `inventory:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/inventory/transfers:
    get: { tags: [Transferências], operationId: listTransfers, summary: Listar transferências (crossdock), description: 'Requer `inventory:read`.', responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { transfers: { type: array, items: { $ref: '#/components/schemas/Transfer' } } } } } } } } }
  /api/v2/inventory/transfers/{id}/dispatch:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    post: { tags: [Transferências], operationId: dispatchTransfer, summary: Despachar (tira estoque da origem), description: 'Requer `inventory:write`. FSM draft→in_transit; 409 se estoque insuficiente.', responses: { '200': { $ref: '#/components/responses/Ok' }, '409': { description: estoque insuficiente/estado inválido } } }
  /api/v2/inventory/transfers/{id}/receive:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    post: { tags: [Transferências], operationId: receiveTransfer, summary: Receber (põe estoque no destino), description: 'Requer `inventory:write`. FSM in_transit→received; re-aponta a reserva do pedido.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/inventory/transfers/{id}/cancel:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    post: { tags: [Transferências], operationId: cancelTransfer, summary: Cancelar (libera reserva), description: 'Requer `inventory:write`. FSM draft→cancelled; cancela o pedido ligado.', responses: { '200': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── CATEGORIAS ───────────────────────────
  /api/v2/catalog/categories:
    get: { tags: [Categorias], operationId: listCategories, summary: Listar categorias, description: 'Requer `catalog:read`.', responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { categories: { type: array, items: { $ref: '#/components/schemas/Category' } } } } } } } } }
    post: { tags: [Categorias], operationId: createCategory, summary: Criar categoria, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CategoryWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/categories/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Categorias], operationId: updateCategory, summary: Atualizar categoria, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CategoryWrite' } } } }, responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Categorias], operationId: deleteCategory, summary: Remover (soft-delete), description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── MARCAS ───────────────────────────
  /api/v2/catalog/brands:
    get: { tags: [Marcas], operationId: listBrands, summary: Listar marcas, description: 'Requer `catalog:read`.', responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { brands: { type: array, items: { $ref: '#/components/schemas/Brand' } } } } } } } } }
    post: { tags: [Marcas], operationId: createBrand, summary: Criar marca, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/BrandWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/brands/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Marcas], operationId: updateBrand, summary: Atualizar marca, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/BrandWrite' } } } }, responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Marcas], operationId: deleteBrand, summary: Remover (soft-delete), description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── ATRIBUTOS ───────────────────────────
  /api/v2/catalog/attributes:
    get: { tags: [Atributos], operationId: listAttributes, summary: Listar atributos, description: 'Requer `catalog:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Atributos], operationId: createAttribute, summary: Criar atributo, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AttributeWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/attributes/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Atributos], operationId: updateAttribute, summary: Atualizar atributo, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Atributos], operationId: deleteAttribute, summary: Remover atributo, description: 'Requer `catalog:write`. Cascata em valores.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/catalog/attributes/values:
    get: { tags: [Atributos], operationId: listAttributeValues, summary: Listar valores de um atributo, description: 'Requer `catalog:read`. Query `attribute_id` obrigatório.', parameters: [ { name: attribute_id, in: query, required: true, schema: { type: string, format: uuid } } ], responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Atributos], operationId: createAttributeValue, summary: Criar valor, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AttributeValueWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/attributes/values/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Atributos], operationId: updateAttributeValue, summary: Atualizar valor, description: 'Requer `catalog:write`. `value`/`hex` são setados incondicionalmente (omitir = NULL).', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Atributos], operationId: deleteAttributeValue, summary: Remover valor, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── VARIAÇÕES ───────────────────────────
  /api/v2/catalog/variation-groups:
    get: { tags: [Variações], operationId: listVariationGroups, summary: Listar grupos de variação, description: 'Requer `catalog:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Variações], operationId: createVariationGroup, summary: Criar grupo de variação, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VariationGroupWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/variation-groups/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Variações], operationId: updateVariationGroup, summary: Atualizar grupo, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Variações], operationId: deleteVariationGroup, summary: Remover grupo, description: 'Requer `catalog:write`. Cascata em termos.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/catalog/variation-terms:
    get: { tags: [Variações], operationId: listVariationTerms, summary: Listar termos de um grupo, description: 'Requer `catalog:read`. Query `variation_group_id` obrigatório.', parameters: [ { name: variation_group_id, in: query, required: true, schema: { type: string, format: uuid } } ], responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Variações], operationId: createVariationTerm, summary: Criar termo, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VariationTermWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/variation-terms/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Variações], operationId: updateVariationTerm, summary: Atualizar termo, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Variações], operationId: deleteVariationTerm, summary: Remover termo, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── GRUPOS DE PRODUTO ───────────────────────────
  /api/v2/catalog/product-groups:
    get: { tags: [Grupos de produto], operationId: listProductGroups, summary: Listar grupos, description: 'Requer `catalog:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Grupos de produto], operationId: createProductGroup, summary: Criar grupo, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProductGroupWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }
  /api/v2/catalog/product-groups/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    put: { tags: [Grupos de produto], operationId: updateProductGroup, summary: Atualizar grupo, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    delete: { tags: [Grupos de produto], operationId: deleteProductGroup, summary: Remover grupo, description: 'Requer `catalog:write`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/catalog/product-groups/{id}/attributes:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    get: { tags: [Grupos de produto], operationId: listGroupAttributes, summary: Eixos de variação do grupo, description: 'Requer `catalog:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Grupos de produto], operationId: addGroupAttribute, summary: Ligar eixo (variation_group) ao grupo, description: 'Requer `catalog:write`. Body `{attribute_id: <variation_group_id>}`.', responses: { '201': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── MÍDIA ───────────────────────────
  /api/v2/catalog/products/{id}/gallery:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    get: { tags: [Mídia], operationId: listGallery, summary: Listar mídia do produto, description: 'Requer `catalog:read`.', responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { gallery: { type: array, items: { $ref: '#/components/schemas/GalleryItem' } } } } } } } } }
    post: { tags: [Mídia], operationId: addGallery, summary: Adicionar imagem/vídeo, description: 'Requer `catalog:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/GalleryWrite' } } } }, responses: { '201': { $ref: '#/components/responses/Created' } } }

  # ─────────────────────────── VENDA-CASADA ───────────────────────────
  /api/v2/catalog/cross-sell/pools:
    get: { tags: [Venda-casada], operationId: listCrossSell, summary: Listar pools, description: 'Requer `catalog:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
    post: { tags: [Venda-casada], operationId: createCrossSell, summary: Criar pool, description: 'Requer `catalog:write`. Body `{name, active?, product_ids[]}`.', responses: { '201': { $ref: '#/components/responses/Created' } } }

  # ─────────────────────────── CLIENTES ───────────────────────────
  /api/v2/customers:
    get:
      tags: [Clientes]
      operationId: listCustomers
      summary: Listar clientes (cursor + delta)
      description: 'Requer `customers:read`. `updated_since` para sync incremental; ABAC pode mascarar PII.'
      parameters: [ { name: updated_since, in: query, schema: { type: string, format: date-time } }, { name: cursor, in: query, schema: { type: string } }, { name: limit, in: query, schema: { type: integer, default: 50, maximum: 250 } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Customer' } }, next_cursor: { type: [string, "null"] } } } } } }, '401': { $ref: '#/components/responses/Unauthorized' } }
    post:
      tags: [Clientes]
      operationId: createCustomer
      summary: Criar cliente (PF/PJ)
      description: 'Requer `customers:write`. Senha de login é gerada (CSPRNG) — o cliente pode redefinir depois.'
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CustomerWrite' }, examples: { pj: { value: { email: "compras@acme.com", name: "ACME", person_type: "pj", document: "12345678000199", legal_name: "ACME LTDA", trade_name: "ACME", state_registration: "ISENTO" } } } } } }
      responses: { '201': { description: Criado, content: { application/json: { schema: { type: object, properties: { id: { type: string, format: uuid }, email: { type: string } } } } } }, '409': { description: e-mail já cadastrado } }
  /api/v2/customers/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    get: { tags: [Clientes], operationId: getCustomer, summary: Ler cliente (perfil completo), description: 'Requer `customers:read`.', responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Customer' } } } }, '404': { $ref: '#/components/responses/NotFound' } } }
    put: { tags: [Clientes], operationId: updateCustomer, summary: Atualizar cliente (parcial), description: 'Requer `customers:write`.', requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CustomerWrite' } } } }, responses: { '200': { $ref: '#/components/responses/Ok' } } }

  # ─────────────────────────── PEDIDOS ───────────────────────────
  /api/v2/orders:
    get: { tags: [Pedidos], operationId: listOrders, summary: Listar pedidos (cursor + delta), description: 'Requer `orders:read`. `updated_since` p/ sync.', parameters: [ { name: updated_since, in: query, schema: { type: string, format: date-time } }, { name: cursor, in: query, schema: { type: string } }, { name: limit, in: query, schema: { type: integer } } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Order' } }, next_cursor: { type: [string, "null"] } } } } } } } }
  /api/v2/orders/{id}:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    get: { tags: [Pedidos], operationId: getOrder, summary: Ler pedido, description: 'Requer `orders:read`.', responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }, '404': { $ref: '#/components/responses/NotFound' } } }
    patch: { tags: [Pedidos], operationId: updateOrder, summary: Atualizar pedido (status/pagamento/notas), description: 'Requer `orders:write`. Body `{status?, payment_status?, notes?}`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/orders/{id}/status:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    patch: { tags: [Pedidos], operationId: setOrderStatus, summary: Mudar status, description: 'Requer `orders:write`. Body `{status}`. `cancelled/refunded` libera estoque; `paid/completed` faz commit.', requestBody: { required: true, content: { application/json: { schema: { type: object, required: [status], properties: { status: { type: string } } } } } }, responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/orders/{id}/ship:
    parameters: [ { $ref: '#/components/parameters/Id' } ]
    post: { tags: [Pedidos], operationId: shipOrder, summary: Enviar (fulfillment), description: 'Requer `orders:write`. Reserva envio no logistics. Body `{carrier, service_code}`.', responses: { '201': { $ref: '#/components/responses/Ok' }, '501': { description: logistics não configurado } } }

  # ─────────────────────────── RELATÓRIOS ───────────────────────────
  /api/v2/reports/abc:
    get: { tags: [Relatórios], operationId: reportABC, summary: Curva ABC, description: 'Requer `reports:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }
  /api/v2/reports/sales:
    get: { tags: [Relatórios], operationId: reportSales, summary: Relatório de vendas, description: 'Requer `reports:read`.', responses: { '200': { $ref: '#/components/responses/Ok' } } }

components:
  parameters:
    Id: { name: id, in: path, required: true, schema: { type: string, format: uuid } }
  responses:
    Ok: { description: OK, content: { application/json: { schema: { type: object } } } }
    Created: { description: Criado, content: { application/json: { schema: { type: object, properties: { id: { type: string, format: uuid } } } } } }
    Unauthorized: { description: 'Token ausente/escopo insuficiente (problem+json).', content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } }
    BadRequest: { description: Corpo/campo inválido, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    NotFound: { description: Não encontrado, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
  securitySchemes:
    OAuth2ClientCredentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://api.venty.me/api/v2/oauth/token
          scopes:
            catalog:read: Ler catálogo (produtos, taxonomia, mídia).
            catalog:write: Escrever catálogo.
            inventory:read: Ler estoque/localizações.
            inventory:write: Escrever estoque/localizações/transferências.
            customers:read: Ler clientes.
            customers:write: Escrever clientes.
            orders:read: Ler pedidos.
            orders:write: Status/fulfillment de pedidos.
            reports:read: Relatórios.
            fiscal:read: Anexar NFe emitida.
  schemas:
    Money: { type: number, description: "Valor em reais (BRL). Ex.: 18.03." }
    TokenRequest: { type: object, required: [grant_type, client_id, client_secret], properties: { grant_type: { type: string, enum: [client_credentials] }, client_id: { type: string }, client_secret: { type: string }, scope: { type: string } } }
    TokenResponse: { type: object, properties: { access_token: { type: string }, token_type: { type: string }, expires_in: { type: integer }, scope: { type: string } } }
    OAuthError: { type: object, properties: { error: { type: string } } }
    # ---------- PRODUTO (todos os campos) ----------
    Product:
      type: object
      description: Produto — resposta completa.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        sku: { type: string }
        price: { oneOf: [ { $ref: '#/components/schemas/Money' }, { type: "null" } ], description: "null em product_type=parent." }
        promotional_price: { oneOf: [ { $ref: '#/components/schemas/Money' }, { type: "null" } ] }
        short_description: { type: [string, "null"] }
        description: { type: [string, "null"], description: "HTML." }
        content: { type: [string, "null"] }
        image_url: { type: [string, "null"] }
        external_image_url: { type: [string, "null"] }
        banner_image: { type: [string, "null"] }
        active: { type: boolean }
        show_product: { type: boolean }
        stock: { type: [integer, "null"], description: "Disponível real; null se não gerenciado." }
        stock_status: { type: [string, "null"], enum: [in_stock, out_of_stock, pre_order, null] }
        stock_mode: { type: string, enum: [global, per_location] }
        manage_stock: { type: boolean }
        low_stock_threshold: { type: integer }
        weight: { type: [number, "null"], description: "kg." }
        length: { type: [number, "null"], description: "cm." }
        width: { type: [number, "null"], description: "cm." }
        height: { type: [number, "null"], description: "cm." }
        is_virtual: { type: boolean }
        is_downloadable: { type: boolean }
        download_file: { type: [string, "null"] }
        product_type: { type: string, enum: [simple, parent, variation] }
        parent_product_id: { type: [string, "null"], format: uuid }
        product_group_id: { type: [string, "null"], format: uuid }
        category_id: { type: [string, "null"], format: uuid }
        brand_id: { type: [string, "null"], format: uuid }
        cost_price: { oneOf: [ { $ref: '#/components/schemas/Money' }, { type: "null" } ], description: "Custo (para margem)." }
        tax_rate: { type: [number, "null"], description: "% de imposto (0..100)." }
        abc_class: { type: [string, "null"], enum: [A, B, C, null] }
        differentiation_attribute_hash: { type: string }
        variation_attributes: { type: object, additionalProperties: { type: string }, description: "{variation_group_id: variation_term_id}." }
        custom_fields: { type: object, description: "Campos personalizados por nicho (JSONB livre)." }
    ProductWrite:
      type: object
      description: |
        Corpo de criar/atualizar produto — CONJUNTO COMPLETO. No POST são obrigatórios
        name/slug/sku (+ price p/ simple/variation). No PUT tudo é opcional; ATENÇÃO:
        promotional_price/short_description/weight/dimensões/category_id/brand_id/cost_price/
        tax_rate são ZERADOS quando omitidos (reenvie-os), enquanto name/slug/sku/price/
        active/stock/manage_stock/product_type são preservados.
      required: [name, slug, sku]
      properties:
        name: { type: string, maxLength: 300 }
        slug: { type: string, maxLength: 300 }
        sku: { type: string, maxLength: 200 }
        price: { $ref: '#/components/schemas/Money' }
        promotional_price: { $ref: '#/components/schemas/Money' }
        short_description: { type: string, maxLength: 500 }
        description: { type: string }
        content: { type: string }
        image_url: { type: string, maxLength: 2048 }
        external_image_url: { type: string, maxLength: 2048 }
        banner_image: { type: string, maxLength: 2048 }
        active: { type: boolean, default: true }
        show_product: { type: boolean, default: true }
        stock: { type: integer, minimum: 0, description: "Estoque inicial (modo global)." }
        stock_status: { type: string, enum: [in_stock, out_of_stock, pre_order] }
        manage_stock: { type: boolean, default: true }
        low_stock_threshold: { type: integer, minimum: 0, default: 5 }
        weight: { type: number, minimum: 0 }
        length: { type: number, minimum: 0 }
        width: { type: number, minimum: 0 }
        height: { type: number, minimum: 0 }
        is_virtual: { type: boolean }
        is_downloadable: { type: boolean }
        download_file: { type: string, maxLength: 2048 }
        product_type: { type: string, enum: [simple, parent, variation], default: simple }
        parent_product_id: { type: string, format: uuid, description: "Obrigatório p/ variation." }
        product_group_id: { type: string, format: uuid }
        category_id: { type: string, format: uuid }
        brand_id: { type: string, format: uuid }
        cost_price: { $ref: '#/components/schemas/Money' }
        tax_rate: { type: number, minimum: 0, maximum: 100 }
        custom_fields: { type: object }
        variation_attributes: { type: object, additionalProperties: { type: string }, description: "{variation_group_id: variation_term_id} (produto variação)." }
        product_attributes:
          type: array
          description: "Atributos do produto; role=differentiation define a variação (compõe o hash)."
          items: { type: object, properties: { attribute_id: { type: string, format: uuid }, value: { type: string }, role: { type: string, enum: [differentiation, grouping, informative], default: informative } } }
    # ---------- ESTOQUE ----------
    Inventory:
      type: object
      properties:
        stock_mode: { type: string, enum: [global, per_location] }
        manage_stock: { type: boolean }
        stock: { type: integer }
        reserved: { type: integer }
        available: { type: integer }
        levels:
          type: array
          items: { type: object, properties: { origin_id: { type: string, format: uuid }, origin_name: { type: string }, origin_type: { type: string, enum: [store, warehouse] }, on_hand: { type: integer }, reserved: { type: integer }, available: { type: integer } } }
    InventoryWrite:
      type: object
      description: "SET absoluto. `stock` (global) OU `levels` (per_location); `stock_mode` opcional."
      properties:
        stock_mode: { type: string, enum: [global, per_location] }
        stock: { type: integer }
        levels: { type: array, items: { type: object, required: [origin_id, on_hand], properties: { origin_id: { type: string, format: uuid }, on_hand: { type: integer } } } }
    # ---------- LOCALIZAÇÃO ----------
    Location:
      type: object
      description: "Loja/depósito. `id` é o `origin_id` dos níveis de estoque."
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type: { type: string, enum: [store, warehouse] }
        address: { type: string }
        number: { type: string }
        complement: { type: string }
        district: { type: string }
        city: { type: string }
        state: { type: string }
        cep: { type: string }
        phone: { type: string }
        responsible: { type: string }
        lat: { type: [number, "null"] }
        lng: { type: [number, "null"] }
        geocoded: { type: boolean }
        service_radius_km: { type: [number, "null"] }
        own_delivery_enabled: { type: boolean }
        own_delivery_pricing: { type: object }
        external_carriers: { type: boolean }
        pickup_enabled: { type: boolean }
        pickup_instructions: { type: string }
        pickup_ready_hours: { type: integer }
        active: { type: boolean }
        order: { type: integer }
    LocationWrite:
      type: object
      required: [name]
      properties:
        name: { type: string }
        type: { type: string, enum: [store, warehouse], default: store }
        address: { type: string }
        number: { type: string }
        complement: { type: string }
        district: { type: string }
        city: { type: string }
        state: { type: string }
        cep: { type: string, description: "8 dígitos; geocoda no save." }
        phone: { type: string }
        responsible: { type: string }
        service_radius_km: { type: number }
        own_delivery_enabled: { type: boolean, description: "Só store." }
        own_delivery_pricing: { type: object, description: "{mode, base_cents, per_km_cents, flat_cents, free_above_cents}." }
        external_carriers: { type: boolean }
        pickup_enabled: { type: boolean, description: "Só store." }
        pickup_instructions: { type: string }
        pickup_ready_hours: { type: integer }
        active: { type: boolean, default: true }
        order: { type: integer }
    # ---------- TRANSFERÊNCIA ----------
    TransferRouteWrite:
      type: object
      required: [from_origin_id, to_origin_id]
      properties:
        from_origin_id: { type: string, format: uuid, description: "Localização que abastece." }
        to_origin_id: { type: string, format: uuid, description: "Localização atendida." }
        lead_time_hours: { type: integer }
        enabled: { type: boolean }
    Transfer:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [draft, in_transit, received, cancelled] }
        from_origin_id: { type: string, format: uuid }
        from_name: { type: string }
        to_origin_id: { type: string, format: uuid }
        to_name: { type: string }
        order_id: { type: [string, "null"], format: uuid }
        order_number: { type: [string, "null"] }
        eta: { type: [string, "null"], format: date-time }
        items: { type: array, items: { type: object, properties: { product_id: { type: string, format: uuid }, product_name: { type: string }, quantity: { type: integer } } } }
    # ---------- TAXONOMIA ----------
    Category: { type: object, properties: { id: { type: string, format: uuid }, name: { type: string }, slug: { type: string }, description: { type: [string, "null"] }, image_url: { type: [string, "null"] }, parent_id: { type: [string, "null"], format: uuid }, order: { type: integer }, active: { type: boolean } } }
    CategoryWrite: { type: object, required: [name, slug], properties: { name: { type: string }, slug: { type: string }, description: { type: string }, image_url: { type: string }, parent_id: { type: string, format: uuid }, order: { type: integer }, active: { type: boolean } } }
    Brand: { type: object, properties: { id: { type: string, format: uuid }, name: { type: string }, slug: { type: string }, logo_url: { type: [string, "null"] }, description: { type: [string, "null"] }, order: { type: integer }, active: { type: boolean } } }
    BrandWrite: { type: object, required: [name, slug], properties: { name: { type: string }, slug: { type: string }, logo_url: { type: string }, description: { type: string }, order: { type: integer }, active: { type: boolean } } }
    AttributeWrite: { type: object, required: [name, slug], properties: { name: { type: string }, slug: { type: string }, layout_type: { type: string, enum: [color_swatch, size_button, radio, dropdown], default: dropdown }, is_filterable: { type: boolean, default: true }, order: { type: integer }, active: { type: boolean } } }
    AttributeValueWrite: { type: object, required: [attribute_id, name, slug], properties: { attribute_id: { type: string, format: uuid }, name: { type: string }, slug: { type: string }, value: { type: string }, hex: { type: string, description: "#RRGGBB." }, order: { type: integer }, active: { type: boolean } } }
    VariationGroupWrite: { type: object, required: [name, slug], properties: { name: { type: string }, slug: { type: string }, display_type: { type: string, default: select }, show_in_listing: { type: boolean, default: false }, order: { type: integer }, active: { type: boolean } } }
    VariationTermWrite: { type: object, required: [variation_group_id, name, slug], properties: { variation_group_id: { type: string, format: uuid }, name: { type: string }, slug: { type: string }, value: { type: string }, image: { type: string }, hex: { type: string }, order: { type: integer }, active: { type: boolean } } }
    ProductGroupWrite: { type: object, required: [name, slug], properties: { name: { type: string }, slug: { type: string }, description: { type: string }, grouping_attribute_id: { type: string, format: uuid }, active: { type: boolean } } }
    GalleryItem: { type: object, properties: { id: { type: string, format: uuid }, type: { type: string, enum: [image, video] }, url: { type: string }, poster_url: { type: [string, "null"] }, is_main: { type: boolean }, order: { type: integer } } }
    GalleryWrite: { type: object, required: [url], properties: { media_type: { type: string, enum: [image, video], default: image }, url: { type: string, description: "https://, http:// ou /media/." }, poster_url: { type: string }, is_main: { type: boolean }, order: { type: integer } } }
    # ---------- CLIENTE ----------
    Customer:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string }
        name: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        document: { type: [string, "null"], description: "CPF (11) ou CNPJ (14), só dígitos." }
        person_type: { type: string, enum: [pf, pj] }
        legal_name: { type: [string, "null"], description: "Razão social (PJ) / nome completo (PF)." }
        trade_name: { type: [string, "null"], description: "Nome fantasia (PJ)." }
        state_registration: { type: [string, "null"], description: "Inscrição estadual." }
        birth_date: { type: [string, "null"], format: date }
        kyc_status: { type: string, enum: [none, pending, approved, rejected] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CustomerWrite:
      type: object
      description: "No create, `email` é obrigatório. Todos os demais são parciais (PUT = parcial)."
      required: [email]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        phone: { type: string }
        document: { type: string, description: "CPF (11 díg) ou CNPJ (14 díg)." }
        person_type: { type: string, enum: [pf, pj] }
        legal_name: { type: string }
        trade_name: { type: string }
        state_registration: { type: string }
        birth_date: { type: string, format: date, description: "YYYY-MM-DD." }
    # ---------- PEDIDO ----------
    Order:
      type: object
      properties:
        id: { type: string, format: uuid }
        number: { type: string }
        status: { type: string, description: "pending|paid|fulfilled|completed|cancelled (canônico externo)." }
        total: { type: object, properties: { amount_cents: { type: integer }, currency: { type: string } } }
        items: { type: array, items: { type: object, properties: { sku: { type: string }, name: { type: string }, quantity: { type: integer }, unit_price: { type: object, properties: { amount_cents: { type: integer }, currency: { type: string } } } } } }
        customer: { type: object, properties: { email: { type: string } } }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Error: { type: object, properties: { error: { type: string }, code: { type: string }, details: { type: object, additionalProperties: { type: string } }, origin_id: { type: string, format: uuid } } }
    Problem: { type: object, properties: { status: { type: integer }, title: { type: string }, detail: { type: string }, trace_id: { type: string } } }
