openapi: 3.0.3
info:
  title: Bolti · API de Integración (e-commerce, POS y ERP)
  version: "2.0.0"
  description: |
    API REST para que un **sistema externo** (tienda online, POS de terceros, ERP)
    opere sobre Bolti: sincronizar catálogo, controlar inventario, emitir
    **facturación electrónica DIAN** (Colombia) y dejar la **contabilidad
    registrada** —asiento, kardex, cartera y recibo de caja— igual que si se
    hubiera hecho en el panel.

    Guía navegable para desarrolladores: `GET /api/public/v1/docs`

    ## Autenticación
    Todas las peticiones (salvo la documentación) requieren una **API key** por
    empresa en el header `x-api-key`:

        x-api-key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    El admin de la empresa la genera desde el panel de Bolti
    (**API & Integraciones**). La key completa se muestra **una sola vez**.

    ## Idempotencia
    Las operaciones de escritura aceptan el header `Idempotency-Key`. Un
    reintento con la misma clave devuelve la respuesta original (header
    `Idempotent-Replay: true`) en vez de duplicar la factura, el asiento y la
    salida de inventario.

    ## Notas DIAN
    - La empresa debe estar **habilitada** (set de pruebas aprobado y resolución
      cargada) para que las facturas se envíen a la DIAN. Consulta `GET /company`.
    - Sin habilitación, se puede facturar con `invoiceClass: "INTERNA"`
      (consecutivo propio, no va a la DIAN).
    - Los **totales se recalculan en el servidor** a partir de los ítems.

servers:
  - url: https://bolti.co/api/public/v1
    description: Producción
  - url: http://localhost:5001/api/public/v1
    description: Local

security:
  - ApiKeyAuth: []

tags:
  - name: Sistema
  - name: Catálogo
  - name: Inventario
  - name: Clientes
  - name: Pedidos
  - name: Facturas
  - name: Cobros y devoluciones
  - name: Contabilidad
  - name: Webhooks
  - name: IA
  - name: Provisioning

paths:
  # ────────────────────────────── Sistema ──────────────────────────────
  /ping:
    get:
      tags: [Sistema]
      summary: Verifica que la API key es válida
      responses:
        "200":
          description: Key válida
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  tenantId: { type: integer, example: 12 }
                  keyName: { type: string, example: "Tienda online" }
                  scopes:
                    type: array
                    items: { type: string }
                  serverTime: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /company:
    get:
      tags: [Sistema]
      summary: Datos de la empresa y estado de facturación electrónica
      description: |
        Lo primero que debe consultar un integrador: si la empresa está habilitada
        ante la DIAN, con qué resolución y en qué rango de numeración.
      responses:
        "200":
          description: Contexto de la empresa
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  company:
                    type: object
                    properties:
                      id: { type: integer }
                      name: { type: string }
                      taxId: { type: string }
                      taxIdType: { type: string }
                  electronicInvoicing:
                    type: object
                    properties:
                      enabled: { type: boolean }
                      testSetStatus: { type: string, example: APROBADO }
                      internalPrefix: { type: string, example: INT }
                      resolution:
                        type: object
                        nullable: true
                        properties:
                          prefix: { type: string }
                          number: { type: string }
                          rangeFrom: { type: integer }
                          rangeTo: { type: integer }
                          validUntil: { type: string, format: date }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /meta:
    get:
      tags: [Sistema]
      summary: Valores válidos de la API
      description: Tipos de documento, métodos de pago, tarifas de IVA en uso, bodegas y unidades.
      responses:
        "200":
          description: Catálogo de valores
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ────────────────────────────── Catálogo ─────────────────────────────
  /products:
    get:
      tags: [Catálogo]
      summary: Listar productos
      description: Requiere scope `products:read`. Para sincronizaciones incrementales usa `updatedSince`.
      parameters:
        - { name: search, in: query, schema: { type: string }, description: "Nombre, SKU, código de barras o referencia" }
        - { name: sku, in: query, schema: { type: string } }
        - { name: barcode, in: query, schema: { type: string } }
        - { name: categoryId, in: query, schema: { type: integer } }
        - { name: active, in: query, schema: { type: boolean } }
        - { name: inStock, in: query, schema: { type: boolean } }
        - { name: updatedSince, in: query, schema: { type: string, format: date-time } }
        - { $ref: "#/components/parameters/Page" }
        - { $ref: "#/components/parameters/PageSize" }
      responses:
        "200":
          description: Catálogo paginado
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  total: { type: integer }
                  page: { type: integer }
                  pageSize: { type: integer }
                  products:
                    type: array
                    items: { $ref: "#/components/schemas/Product" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Catálogo]
      summary: Crear o actualizar un producto (upsert por SKU)
      description: |
        Requiere scope `products:write`. Lo que no se envía no se modifica.
        Si se envía `stock`, el saldo se ajusta **vía kardex** (movimiento +
        asiento contable); nunca se sobrescribe la tabla directamente.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProductInput" }
      responses:
        "200": { description: Producto actualizado }
        "201": { description: Producto creado }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /products/bulk:
    post:
      tags: [Catálogo]
      summary: Sincronización masiva (hasta 100 productos)
      description: |
        Requiere scope `products:write`. Cada producto se procesa en su propia
        transacción: un SKU inválido no tumba el lote. Responde `207` si alguno falló.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [products]
              properties:
                products:
                  type: array
                  maxItems: 100
                  items: { $ref: "#/components/schemas/ProductInput" }
      responses:
        "200": { description: Todo sincronizado }
        "207": { description: Sincronizado con errores parciales }
        "400": { $ref: "#/components/responses/BadRequest" }

  /products/{idOrSku}:
    parameters:
      - { name: idOrSku, in: path, required: true, schema: { type: string }, description: "Id numérico, SKU, referencia o código de barras" }
    get:
      tags: [Catálogo]
      summary: Detalle del producto con existencias por bodega
      responses:
        "200":
          description: Producto
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  product: { $ref: "#/components/schemas/Product" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Catálogo]
      summary: Actualización parcial
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProductInput" }
      responses:
        "200": { description: Producto actualizado }
        "404": { $ref: "#/components/responses/NotFound" }

  /categories:
    get:
      tags: [Catálogo]
      summary: Listar categorías
      responses:
        "200": { description: Categorías del catálogo }

  # ───────────────────────────── Inventario ────────────────────────────
  /stock:
    get:
      tags: [Inventario]
      summary: Existencias disponibles
      description: Requiere scope `inventory:read`. Es lo que se consulta en el checkout.
      parameters:
        - { name: sku, in: query, schema: { type: string }, description: "Uno o varios separados por coma" }
        - { name: productId, in: query, schema: { type: integer } }
        - { name: lowStock, in: query, schema: { type: boolean }, description: "Solo lo que está en el mínimo o por debajo" }
        - { name: updatedSince, in: query, schema: { type: string, format: date-time } }
        - { $ref: "#/components/parameters/Page" }
        - { $ref: "#/components/parameters/PageSize" }
      responses:
        "200":
          description: Existencias
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  stock:
                    type: array
                    items:
                      type: object
                      properties:
                        productId: { type: integer }
                        sku: { type: string }
                        name: { type: string }
                        available: { type: number, example: 25 }
                        minStock: { type: number, nullable: true }
                        lowStock: { type: boolean }
                        averageCost: { type: number }

  /inventory/adjustments:
    post:
      tags: [Inventario]
      summary: Entrada, salida o conteo físico
      description: |
        Requiere scope `inventory:write`. Genera movimiento de kardex **y** asiento contable:
        `IN` D inventario / C proveedores · `OUT` D costo / C inventario ·
        `SET` ajusta contra ingresos o pérdidas según sobrante o faltante.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, quantity]
              properties:
                sku: { type: string, example: "CAM-AZUL-M" }
                productId: { type: integer }
                type: { type: string, enum: [IN, OUT, SET] }
                quantity: { type: number, example: 10 }
                unitCost: { type: number, example: 42000 }
                date: { type: string, format: date }
                reason: { type: string, example: "Entrada de proveedor OC-338" }
                reference: { type: string, example: "OC-338" }
                warehouseId: { type: integer }
      responses:
        "201":
          description: Movimiento registrado
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  movement:
                    type: object
                    properties:
                      id: { type: integer }
                      type: { type: string }
                      quantity: { type: number }
                      balanceAfter: { type: number }
                      journalEntryId: { type: integer, nullable: true }
                  product: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /inventory/movements:
    get:
      tags: [Inventario]
      summary: Kardex
      parameters:
        - { name: sku, in: query, schema: { type: string } }
        - { name: productId, in: query, schema: { type: integer } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: type, in: query, schema: { type: string, enum: [IN, OUT, ADJUST] } }
      responses:
        "200": { description: Movimientos con saldo y costo promedio }

  /warehouses:
    get:
      tags: [Inventario]
      summary: Bodegas activas
      responses:
        "200": { description: Bodegas de la empresa }

  # ────────────────────────────── Clientes ─────────────────────────────
  /customers:
    get:
      tags: [Clientes]
      summary: Listar clientes
      parameters:
        - { name: search, in: query, schema: { type: string } }
        - { name: documentNumber, in: query, schema: { type: string } }
        - { name: email, in: query, schema: { type: string } }
        - { name: updatedSince, in: query, schema: { type: string, format: date-time } }
        - { $ref: "#/components/parameters/Page" }
        - { $ref: "#/components/parameters/PageSize" }
      responses:
        "200": { description: Clientes (terceros con rol CUSTOMER) }
    post:
      tags: [Clientes]
      summary: Crear o actualizar cliente (upsert por documento)
      description: |
        Requiere scope `customers:write`. No es obligatorio llamarlo antes de
        `POST /orders`: el pedido ya crea el tercero.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomerInput" }
      responses:
        "201": { description: Cliente creado o actualizado }
        "400": { $ref: "#/components/responses/BadRequest" }

  /customers/{documentNumber}:
    get:
      tags: [Clientes]
      summary: Buscar cliente por documento
      parameters:
        - { name: documentNumber, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Cliente }
        "404": { $ref: "#/components/responses/NotFound" }

  # ────────────────────────────── Pedidos ──────────────────────────────
  /orders:
    post:
      tags: [Pedidos]
      summary: Facturar un pedido (endpoint recomendado para e-commerce)
      description: |
        En una sola llamada: crea/actualiza el cliente, resuelve las líneas por SKU,
        valida inventario, emite la factura con **asiento contable, salida de kardex,
        cuenta por cobrar y recibo de caja** (si viene pagada) y la envía a la DIAN.

        Requiere scope `invoices:write`. Envía siempre `externalId` y `Idempotency-Key`.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderInput" }
            examples:
              pagado:
                summary: Pedido pagado con tarjeta
                value:
                  externalId: "SHOP-1042"
                  customer:
                    name: "María Restrepo"
                    documentType: "CC"
                    documentNumber: "1017234567"
                    email: "maria@ejemplo.com"
                  items:
                    - { sku: "CAM-AZUL-M", quantity: 2 }
                  shipping: { amount: 12000, taxRate: 0 }
                  payment: { status: "PAID", method: "TARJETA", reference: "wompi_01HX" }
              credito:
                summary: Pedido a crédito a 30 días
                value:
                  externalId: "SHOP-1043"
                  customer:
                    name: "Comercializadora ACME S.A.S."
                    documentType: "NIT"
                    documentNumber: "900123456"
                  items:
                    - { sku: "CAM-AZUL-M", quantity: 10, unitPrice: 70000, taxRate: 19 }
                  payment: { status: "PENDING", creditTermDays: 30 }
      responses:
        "201":
          description: Pedido facturado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrderResult" }
        "200":
          description: El pedido ya se había facturado (mismo `externalId`)
        "400": { $ref: "#/components/responses/BadRequest" }
        "409":
          description: Sin inventario suficiente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: false }
                  error: { type: string }
                  stockIssues:
                    type: array
                    items:
                      type: object
                      properties:
                        sku: { type: string }
                        requested: { type: number }
                        available: { type: number }

  /orders/{externalId}:
    get:
      tags: [Pedidos]
      summary: Consultar un pedido por el id de la tienda
      parameters:
        - { name: externalId, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Pedido, estado DIAN y radiografía contable }
        "404": { $ref: "#/components/responses/NotFound" }

  # ────────────────────────────── Facturas ─────────────────────────────
  /invoices:
    post:
      tags: [Facturas]
      summary: Emitir una factura (modo crudo)
      description: |
        Para integraciones que ya arman la factura completa: no resuelve SKU ni crea
        el cliente. Si vienes de cero, usa `POST /orders`. Requiere scope `invoices:write`.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InvoiceInput" }
      responses:
        "200":
          description: Factura creada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InvoiceResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
    get:
      tags: [Facturas]
      summary: Listar facturas
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: dianStatus, in: query, schema: { type: string } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: customerDocument, in: query, schema: { type: string } }
        - { name: search, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/Page" }
        - { $ref: "#/components/parameters/PageSize" }
      responses:
        "200":
          description: Facturas paginadas
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  total: { type: integer }
                  invoices:
                    type: array
                    items: { $ref: "#/components/schemas/Invoice" }

  /invoices/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string }, description: "Id, número de factura o externalId" }
    get:
      tags: [Facturas]
      summary: Detalle de una factura
      description: |
        Incluye las líneas y, si la DIAN la rechazó, los motivos en `dian.messages`
        (código de regla + descripción). La respuesta interna del proveedor no se expone.
      responses:
        "200": { description: Factura con líneas y motivos DIAN }
        "404": { $ref: "#/components/responses/NotFound" }

  /invoices/{id}/status:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [Facturas]
      summary: Estado DIAN (respuesta ligera para polling)
      responses:
        "200":
          description: Estado
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  id: { type: integer }
                  invoiceNumber: { type: string }
                  externalId: { type: string, nullable: true }
                  status: { type: string }
                  paymentStatus: { type: string }
                  dianStatus: { type: string, nullable: true }
                  cufe: { type: string, nullable: true }
        "404": { $ref: "#/components/responses/NotFound" }

  /invoices/{id}/accounting:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [Contabilidad]
      summary: Radiografía contable de la factura
      description: Asiento generado con sus líneas, si cuadra, y estado de la cuenta por cobrar.
      responses:
        "200": { description: Asiento y cartera }
        "404": { $ref: "#/components/responses/NotFound" }

  # ──────────────────── Cobros y devoluciones ─────────────────────────
  /invoices/{id}/payments:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [Cobros y devoluciones]
      summary: Registrar un cobro
      description: |
        Recibo de caja que cruza la cartera (D caja/bancos / C clientes).
        Sin `amount` aplica el saldo pendiente completo. Requiere scope `payments:write`.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [method]
              properties:
                method: { type: string, enum: [EFECTIVO, TRANSFERENCIA, TARJETA, CHEQUE] }
                amount: { type: number, description: "Por defecto, el saldo pendiente" }
                date: { type: string, format: date }
                reference: { type: string }
                bankName: { type: string }
                notes: { type: string }
                withholdings:
                  type: object
                  properties:
                    retefuente: { type: number }
                    reteiva: { type: number }
                    reteica: { type: number }
      responses:
        "201": { description: Pago registrado }
        "409": { description: El pago supera el saldo pendiente }
    get:
      tags: [Cobros y devoluciones]
      summary: Pagos aplicados a una factura
      responses:
        "200": { description: Recibos y saldo de cartera }

  /invoices/{id}/returns:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [Cobros y devoluciones]
      summary: Devolución (nota crédito electrónica)
      description: |
        Emite la nota crédito, reingresa el inventario y reversa el costo de venta.
        Sin `items` devuelve la factura completa. Requiere scope `creditnotes:write`.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, example: "Devolución" }
                date: { type: string, format: date }
                notes: { type: string }
                items:
                  type: array
                  items:
                    type: object
                    properties:
                      sku: { type: string }
                      quantity: { type: number }
      responses:
        "201": { description: Nota crédito creada }
        "400": { $ref: "#/components/responses/BadRequest" }

  /credit-notes:
    get:
      tags: [Cobros y devoluciones]
      summary: Listar notas crédito
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: invoiceNumber, in: query, schema: { type: string } }
      responses:
        "200": { description: Notas crédito con su estado DIAN }

  # ──────────────────────────── Contabilidad ──────────────────────────
  /accounting/journal:
    get:
      tags: [Contabilidad]
      summary: Libro diario
      description: Requiere scope `accounting:read`.
      parameters:
        - { name: documentType, in: query, schema: { type: string }, example: FACTURA }
        - { name: documentNumber, in: query, schema: { type: string } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
      responses:
        "200": { description: Asientos con sus líneas }

  /accounting/receivables:
    get:
      tags: [Contabilidad]
      summary: Cartera (cuentas por cobrar)
      parameters:
        - { name: documentNumber, in: query, schema: { type: string }, description: "Documento del cliente" }
        - { name: onlyOpen, in: query, schema: { type: boolean, default: true } }
      responses:
        "200": { description: Cuentas por cobrar con saldo }

  # ───────────────────────────── Webhooks ─────────────────────────────
  /webhooks:
    get:
      tags: [Webhooks]
      summary: Listar webhooks y eventos disponibles
      responses:
        "200": { description: Webhooks registrados }
    post:
      tags: [Webhooks]
      summary: Registrar un webhook
      description: |
        Devuelve el `secret` **una sola vez**. Cada entrega llega firmada en
        `x-bolti-signature: t=<unix>,v1=<HMAC_SHA256(secret, "<t>.<cuerpo>")>`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, example: "https://mitienda.com/hooks/bolti" }
                description: { type: string }
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - invoice.created
                      - invoice.dian.accepted
                      - invoice.dian.rejected
                      - payment.created
                      - creditnote.created
                      - stock.updated
                      - stock.low
      responses:
        "201": { description: Webhook creado (incluye el secreto) }
        "400": { $ref: "#/components/responses/BadRequest" }

  /webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Eliminar un webhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: Eliminado }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Enviar una entrega de prueba
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: Resultado de la entrega (código HTTP que respondió tu servidor) }

  /webhooks/deliveries:
    get:
      tags: [Webhooks]
      summary: Historial de entregas
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [PENDING, DELIVERED, FAILED] } }
        - { name: event, in: query, schema: { type: string } }
      responses:
        "200": { description: Últimas 100 entregas }

  # ──────────────────────────────── IA ────────────────────────────────
  /ai/chat:
    post:
      tags: [IA]
      summary: Conversar con la IA del negocio
      description: Requiere scope `ai:chat`. Usa la credencial del tenant (BYOK) o sus créditos de Bolti.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message: { type: string }
                messages:
                  type: array
                  items:
                    type: object
                    properties:
                      role: { type: string, enum: [user, assistant] }
                      content: { type: string }
                context: { type: string, description: "Datos del negocio para que el modelo los use" }
      responses:
        "200": { description: Respuesta del modelo }
        "402": { description: Sin créditos de IA }

  # ───────────────────────────── Provisioning ─────────────────────────
  /tenants:
    post:
      tags: [Provisioning]
      summary: Crear o vincular una empresa hija
      description: |
        Modo "empresa madre" (marketplaces, holdings, contadores). Idempotente por NIT:
        si ya existe, se vincula conservando su data. Devuelve la API key de la hija
        una sola vez. Requiere scope `tenants:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [businessName, taxId]
              properties:
                businessName: { type: string }
                taxId: { type: string }
                taxIdType: { type: string, default: NIT }
                email: { type: string }
                needsElectronicInvoice: { type: boolean }
      responses:
        "201": { description: Empresa creada }
        "200": { description: Empresa existente vinculada }
    get:
      tags: [Provisioning]
      summary: Listar empresas hijas
      responses:
        "200": { description: Empresas administradas }

  /tenants/{id}:
    get:
      tags: [Provisioning]
      summary: Detalle de una empresa hija
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        "200": { description: Detalle + checklist de onboarding }

  /tenants/{id}/dian/register:
    post:
      tags: [Provisioning]
      summary: Registrar la empresa ante el proveedor DIAN
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        "200": { description: Registro solicitado }

  /tenants/{id}/dian/test-set:
    post:
      tags: [Provisioning]
      summary: Enviar el set de pruebas
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [testSetId]
              properties:
                testSetId: { type: string, description: "Se obtiene a mano en el portal MUISCA de la DIAN" }
      responses:
        "200": { description: Set enviado }

  /tenants/{id}/dian/status:
    get:
      tags: [Provisioning]
      summary: Estado de habilitación
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        "200": { description: Estado del onboarding }

  /tenants/{id}/dian/resolution:
    post:
      tags: [Provisioning]
      summary: Traer y guardar la resolución de facturación
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        "200": { description: Resolución guardada }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

  parameters:
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    PageSize:
      name: pageSize
      in: query
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 200 }
      description: |
        Clave única de la operación (por ejemplo el id del pedido). Reintentar con la
        misma clave devuelve la respuesta original en vez de duplicar el documento.

  responses:
    BadRequest:
      description: Petición inválida
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: API key ausente, inválida, revocada o expirada
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: La API key no tiene el scope requerido
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Recurso no encontrado
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        error: { type: string, example: "No hay inventario suficiente para el pedido." }

    Product:
      type: object
      properties:
        id: { type: integer, example: 91 }
        sku: { type: string, example: "CAM-AZUL-M" }
        barcode: { type: string, nullable: true }
        reference: { type: string, nullable: true }
        name: { type: string }
        description: { type: string, nullable: true }
        brand: { type: string, nullable: true }
        categoryId: { type: integer, nullable: true }
        categoryName: { type: string, nullable: true }
        price: { type: number, description: "Base gravable (sin IVA)", example: 75546 }
        priceWithTax: { type: number, example: 89900 }
        priceIncludesTax: { type: boolean }
        taxRate: { type: number, example: 19 }
        cost: { type: number, description: "Costo promedio", example: 42000 }
        unit: { type: string, example: "und" }
        stock: { type: number, example: 25 }
        minStock: { type: number, nullable: true }
        tracksInventory: { type: boolean }
        active: { type: boolean }
        imageUrl: { type: string, nullable: true }
        accounts:
          type: object
          properties:
            revenue: { type: string, nullable: true, example: "413536" }
            inventory: { type: string, nullable: true, example: "143505" }
            cost: { type: string, nullable: true, example: "613536" }
        updatedAt: { type: string, format: date-time }

    ProductInput:
      type: object
      required: [sku]
      properties:
        sku: { type: string, example: "CAM-AZUL-M" }
        name: { type: string, description: "Obligatorio al crear" }
        description: { type: string }
        price: { type: number, example: 89900 }
        priceIncludesTax: { type: boolean, example: true }
        taxRate: { type: number, example: 19 }
        cost: { type: number, example: 42000 }
        stock: { type: number, description: "Fija el saldo. El ajuste entra por kardex con su asiento" }
        minStock: { type: number }
        unit: { type: string, example: "und" }
        barcode: { type: string }
        reference: { type: string }
        brand: { type: string }
        categoryId: { type: integer }
        categoryName: { type: string, description: "Se crea si no existe" }
        tracksInventory: { type: boolean, default: true }
        active: { type: boolean, default: true }
        imageUrl: { type: string }
        weight: { type: number }
        warehouseId: { type: integer }
        accounts:
          type: object
          properties:
            revenue: { type: string }
            inventory: { type: string }
            cost: { type: string }

    CustomerInput:
      type: object
      required: [name, documentNumber]
      properties:
        name: { type: string, example: "María Restrepo" }
        documentType: { type: string, enum: [CC, NIT, CE, TI, PA, RC], default: CC }
        documentNumber: { type: string, example: "1017234567" }
        dv: { type: string, description: "Dígito de verificación del NIT" }
        email: { type: string }
        phone: { type: string }
        address:
          type: object
          properties:
            address: { type: string }
            city: { type: string }
            department: { type: string }

    OrderItem:
      type: object
      required: [quantity]
      properties:
        sku: { type: string, description: "Recomendado: hereda precio, IVA, cuenta de ingreso y descuenta inventario" }
        productId: { type: integer }
        description: { type: string, description: "Obligatorio si no hay sku" }
        quantity: { type: number, example: 2 }
        unitPrice: { type: number, description: "Si se omite, el precio del catálogo" }
        discount: { type: number, description: "Porcentaje por línea", example: 10 }
        taxRate: { type: number, example: 19 }
        revenueAccount: { type: string, description: "Cuenta de ingreso específica de la línea" }

    OrderInput:
      type: object
      required: [customer, items]
      properties:
        externalId: { type: string, description: "Id del pedido en tu tienda. Evita duplicados de forma permanente" }
        date: { type: string, format: date }
        dueDate: { type: string, format: date }
        invoiceClass: { type: string, enum: [ELECTRONICA, INTERNA], default: ELECTRONICA }
        customer: { $ref: "#/components/schemas/CustomerInput" }
        items:
          type: array
          items: { $ref: "#/components/schemas/OrderItem" }
        shipping:
          type: object
          description: Se factura como una línea más
          properties:
            amount: { type: number, example: 12000 }
            taxRate: { type: number, example: 0 }
            description: { type: string, default: "Envío" }
        payment:
          type: object
          properties:
            status: { type: string, enum: [PAID, PENDING], default: PENDING }
            method: { type: string, enum: [EFECTIVO, TRANSFERENCIA, TARJETA, CHEQUE, CREDITO] }
            reference: { type: string, description: "Id de la pasarela de pago" }
            creditTermDays: { type: integer, default: 30 }
        allowNegativeStock: { type: boolean, default: false }
        notes: { type: string }

    OrderResult:
      type: object
      properties:
        success: { type: boolean }
        order:
          type: object
          properties:
            externalId: { type: string, nullable: true }
            invoiceId: { type: integer }
            invoiceNumber: { type: string }
            total: { type: number }
            paid: { type: boolean }
            paymentMethod: { type: string }
        customer:
          type: object
          properties:
            id: { type: integer }
            name: { type: string }
            documentNumber: { type: string }
        dian:
          type: object
          nullable: true
          properties:
            sent: { type: boolean }
            dianStatus: { type: string, nullable: true }
            cufe: { type: string, nullable: true }
            error: { type: string, nullable: true }
        accounting:
          type: object
          nullable: true
          properties:
            total: { type: number }
            paymentStatus: { type: string }
            journalEntry:
              type: object
              nullable: true
              properties:
                id: { type: integer }
                number: { type: string }
                balanced: { type: boolean }
                totalDebit: { type: number }
                totalCredit: { type: number }
                lines:
                  type: array
                  items:
                    type: object
                    properties:
                      accountCode: { type: string }
                      accountName: { type: string }
                      debit: { type: number }
                      credit: { type: number }
            receivable:
              type: object
              nullable: true
              properties:
                id: { type: integer }
                total: { type: number }
                paid: { type: number }
                balance: { type: number }
                status: { type: string }

    InvoiceInput:
      type: object
      required: [customer, items]
      properties:
        invoiceClass: { type: string, enum: [ELECTRONICA, INTERNA], default: ELECTRONICA }
        number: { type: string, description: "Opcional. Si se omite, Bolti asigna el consecutivo" }
        date: { type: string, format: date }
        dueDate: { type: string, format: date }
        paymentForm: { type: string, enum: [CONTADO, CREDITO] }
        paymentMethod: { type: string, example: EFECTIVO }
        creditTermDays: { type: integer }
        customer:
          type: object
          required: [name, identification]
          properties:
            name: { type: string }
            identificationType: { type: string, example: NIT }
            identification: { type: string, example: "900123456" }
            email: { type: string }
            phone: { type: string }
            address:
              type: object
              properties:
                address: { type: string }
                city: { type: string }
                department: { type: string }
        items:
          type: array
          items:
            type: object
            required: [description, quantity, unitPrice]
            properties:
              productId: { type: integer }
              description: { type: string }
              quantity: { type: number }
              unitPrice: { type: number, description: "Antes de impuestos" }
              discount: { type: number, description: "Porcentaje por línea" }
              taxRate: { type: number, example: 19 }
        notes: { type: string }
        copyEmail: { type: string, description: "Correo CC opcional para el envío del PDF" }

    InvoiceResult:
      type: object
      properties:
        success: { type: boolean }
        message: { type: string }
        invoice:
          type: object
          properties:
            id: { type: integer }
            invoiceNumber: { type: string }
        dian:
          type: object
          nullable: true
          properties:
            sent: { type: boolean }
            cufe: { type: string, nullable: true }
            dianStatus: { type: string, nullable: true }
            error: { type: string, nullable: true }

    Invoice:
      type: object
      properties:
        id: { type: integer }
        invoiceNumber: { type: string }
        invoiceClass: { type: string }
        externalId: { type: string, nullable: true }
        status: { type: string }
        customer:
          type: object
          properties:
            name: { type: string }
            documentType: { type: string }
            documentNumber: { type: string }
            email: { type: string, nullable: true }
        date: { type: string, format: date }
        dueDate: { type: string, format: date }
        totals:
          type: object
          properties:
            subtotal: { type: number }
            discount: { type: number }
            tax: { type: number }
            total: { type: number }
        paymentMethod: { type: string, nullable: true }
        paymentStatus: { type: string }
        dian:
          type: object
          properties:
            status: { type: string, nullable: true }
            cufe: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
