openapi: 3.0.3
info:
  title: SICOVI API v1
  version: 1.0.0
  description: |
    ## Autenticación

    Header `Authorization: Bearer sk_live_…`. El token lo emite el administrador
    en Integraciones y queda atado a una empresa. Rate limit: 120 peticiones
    por minuto por token (HTTP 429).

    ## Escrituras

    Toda mutación POST de ventas debe enviar `Idempotency-Key` (UUID propio).
    Si reenvías la misma clave, se replaya la respuesta. Esa clave también
    alimenta el ledger del POS.

    ## Códigos

    - `401` token inválido o revocado
    - `402` suscripción inactiva (solo escrituras SaaS)
    - `403` scope insuficiente o vigencia no SaaS
    - `409` Idempotency-Key reutilizada con otro cuerpo de venta
    - `422` regla de negocio (sin stock, pagos, tienda ajena)
    - `429` rate limit

    ## Alcance

    Lectura y alta de catálogo (producto/cliente), consulta de inventario y
    venta de contado vía el pipeline POS. No incluye timbrar ni cancelar SAT,
    borrar registros, movimientos de existencias, caja ni precios 2–5.

    ## Documentación

    Manual HTML: `/api/docs`. Try it out (Swagger): `/api/swagger`.
    Spec OpenAPI: `/api/openapi.yaml`. Colección Postman: `/api/postman.json`.

    ## Webhooks

    Eventos: `catalog.updated`, `customer.updated`, `sale.created`,
    `document.stamped`, `sale.paid`. Firma HMAC-SHA256 en `X-Sicovi-Signature`
    del body JSON. Reintento: `php artisan api:retry-webhooks` cada 5 min.
servers:
  - url: https://sicovi.com.mx/api/v1
security:
  - bearerAuth: []
tags:
  - name: meta
    description: Salud del token y empresa activa.
  - name: catalog
    description: Unidades, departamentos y marcas de la empresa (solo lectura).
  - name: products
    description: Catálogo de productos (lectura y alta).
  - name: inventory
    description: Almacenes, existencias, tiendas y formas de pago.
  - name: customers
    description: Clientes fiscales (RFC, CFDI, contacto).
  - name: sales
    description: Ventas de contado e idempotencia.
  - name: documents
    description: CFDI emitidos (consulta, XML y PDF).
  - name: events
    description: Eventos internos para webhooks.
paths:
  /meta/ping:
    get:
      tags: [meta]
      summary: Health y empresa del token
      responses:
        '200':
          description: OK
          content:
            application/json:
              example: { ok: true, empresa_id: 1, scopes: ['catalog:read'] }
        '401':
          description: Token inválido
  /products:
    get:
      tags: [products]
      summary: Listar productos
      parameters:
        - in: query
          name: updated_since
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Product' }
    post:
      tags: [products]
      summary: Crear producto
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductWrite'
            example:
              name: Playera
              code: PLY-001
              price: 100
              cost: 40
              status: 1
      responses:
        '201': { description: Creado }
        '403': { description: Scope insuficiente }
  /products/{id}:
    get:
      tags: [products]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: Producto }
        '404': { description: No encontrado }
    post:
      tags: [products]
      summary: Actualizar producto
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductWrite'
            example:
              name: Playera actualizada
              price: 120
      responses:
        '200': { description: Actualizado }
  /warehouses:
    get:
      tags: [inventory]
      responses:
        '200': { description: Almacenes }
  /stock:
    get:
      tags: [inventory]
      parameters:
        - in: query
          name: warehouse_id
          schema: { type: integer }
      responses:
        '200': { description: Existencias }
  /stores:
    get:
      tags: [sales]
      summary: Establecimientos (tiendas) activos
      responses:
        '200': { description: Lista }
  /payment-methods:
    get:
      tags: [sales]
      summary: Formas de pago activas
      description: Catálogo global del POS (Estatus=1, sin CodigoSAT 99). No se filtra por empresa.
      responses:
        '200': { description: Lista }
  /units:
    get:
      tags: [catalog]
      summary: Unidades de medida de la empresa
      responses:
        '200': { description: Lista }
  /departments:
    get:
      tags: [catalog]
      summary: Departamentos de la empresa
      responses:
        '200': { description: Lista }
  /brands:
    get:
      tags: [catalog]
      summary: Marcas de la empresa
      responses:
        '200': { description: Lista }
  /customers:
    get:
      tags: [customers]
      parameters:
        - in: query
          name: updated_since
          schema: { type: string, format: date-time }
      responses:
        '200': { description: Lista }
    post:
      tags: [customers]
      summary: Crear cliente
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerWrite'
            example:
              name: Cliente demo
              rfc: XAXX010101000
              email: cliente@example.com
              cfdi_use: G03
              tax_regime: '616'
              status: 1
      responses:
        '201': { description: Creado }
  /customers/{id}:
    get:
      tags: [customers]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: Cliente }
    post:
      tags: [customers]
      summary: Actualizar cliente
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerWrite'
            example:
              name: Cliente actualizado
              email: nuevo@example.com
      responses:
        '200': { description: Actualizado }
  /sales:
    get:
      tags: [sales]
      parameters:
        - in: query
          name: updated_since
          schema: { type: string, format: date-time }
      responses:
        '200': { description: Ventas }
    post:
      tags: [sales]
      summary: Alta de venta con partidas e inventario (pipeline POS)
      description: >
        `price` de cada partida debe coincidir con PrecioVenta del producto
        (salvo que el usuario POS pueda modificar precio). El pago debe cubrir
        el total con IVA. Header Idempotency-Key obligatorio.
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaleCreate'
            example:
              store_id: 1
              customer_id: 2
              date: '2026-08-12'
              credit: false
              lines:
                - { product_id: 10, qty: 1, price: 100 }
              payments:
                - { payment_method_id: 1, amount: 116 }
      responses:
        '201': { description: Creada. El replay de la misma Idempotency-Key reenvía este mismo cuerpo y status. }
        '400': { description: Falta Idempotency-Key }
        '409': { description: Clave usada con otro cuerpo }
        '422': { description: Regla de negocio }
  /sales/{id}:
    get:
      tags: [sales]
      summary: Venta con partidas y pagos
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: Venta }
  /documents:
    get:
      tags: [documents]
      summary: CFDI emitidos
      parameters:
        - in: query
          name: updated_since
          schema: { type: string, format: date-time }
        - in: query
          name: uuid
          schema: { type: string }
      responses:
        '200': { description: Documentos }
  /documents/{id}:
    get:
      tags: [documents]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: Documento }
  /documents/{id}/xml:
    get:
      tags: [documents]
      summary: Descargar XML del CFDI
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: XML }
        '404': { description: Archivo no persistido }
  /documents/{id}/pdf:
    get:
      tags: [documents]
      summary: Descargar PDF del CFDI
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: PDF }
        '404': { description: Archivo no persistido }
  /events:
    get:
      tags: [events]
      parameters:
        - in: query
          name: since
          schema: { type: string, format: date-time }
      responses:
        '200': { description: Event log }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Token sk_live_ emitido en Integraciones. Header Idempotency-Key en escrituras.
  schemas:
    Product:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        name: { type: string }
        status: { type: integer }
        price: { type: number }
        cost: { type: number }
        unit_id: { type: integer }
        sat_code: { type: string }
        causes_iva: { type: boolean }
        is_service: { type: boolean }
        lot_control: { type: boolean, description: ControlLote }
        causes_ieps: { type: boolean, description: CausaIEPS }
        presentation: { type: string, description: Presentacion }
        min_qty: { type: number, description: Minimo }
        location: { type: string, description: Ubicacion }
        sat_code_name: { type: string, description: DescripcionCodigoSAT }
        department_id: { type: integer }
        brand_id: { type: integer }
        updated_at: { type: string }
    ProductWrite:
      type: object
      properties:
        name: { type: string, description: Descripcion }
        code: { type: string, description: CodigoProducto }
        price: { type: number, description: PrecioVenta }
        cost: { type: number, description: Costo }
        status: { type: integer, description: Estatus }
        unit_id: { type: integer, description: IdUnidad }
        sat_code: { type: string, description: CodigoSAT }
        causes_iva: { type: boolean, description: CausaIva }
        is_service: { type: boolean, description: EsServicio }
        lot_control: { type: boolean, description: ControlLote }
        causes_ieps: { type: boolean, description: CausaIEPS }
        presentation: { type: string, description: Presentacion }
        min_qty: { type: number, description: Minimo }
        location: { type: string, description: Ubicacion }
        department_id: { type: integer }
        brand_id: { type: integer }
    Customer:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        rfc: { type: string }
        email: { type: string }
        status: { type: integer }
        cfdi_use: { type: string }
        tax_regime: { type: string }
        phone: { type: string }
        address: { type: string }
        neighborhood: { type: string }
        city: { type: string }
        state: { type: string }
        country: { type: string }
        postal_code: { type: string }
        num_ext: { type: string }
        num_int: { type: string }
        contact_name: { type: string }
        credit: { type: boolean }
        payment_method_id: { type: integer }
        credit_days: { type: integer }
        credit_limit: { type: number, description: LimiteCredito }
        price_list: { type: integer, description: ListaPrecios (1-5) }
        curp: { type: string }
        contact_phone: { type: string, description: TelefonoContacto }
        street1: { type: string, description: EntreCalle1 }
        street2: { type: string, description: EntreCalle2 }
        notes: { type: string, description: Observaciones }
        is_employee: { type: boolean, description: EsEmpleado }
        cfdi_use_name: { type: string, description: DescripcionUsoCFDI }
    CustomerWrite:
      type: object
      properties:
        name: { type: string }
        rfc: { type: string }
        email: { type: string, description: Email }
        cfdi_use: { type: string, description: UsoCFDI }
        tax_regime: { type: string, description: DescripcionRegimenFiscal }
        status: { type: integer }
        phone: { type: string }
        address: { type: string }
        neighborhood: { type: string }
        city: { type: string }
        state: { type: string }
        country: { type: string }
        postal_code: { type: string }
        num_ext: { type: string }
        num_int: { type: string }
        contact_name: { type: string }
        credit: { type: boolean }
        payment_method_id: { type: integer }
        credit_days: { type: integer }
        credit_limit: { type: number }
        price_list: { type: integer }
        curp: { type: string }
        contact_phone: { type: string }
        street1: { type: string }
        street2: { type: string }
        notes: { type: string }
        is_employee: { type: boolean }
    Warehouse:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        status: { type: integer }
        type_id: { type: integer }
    Store:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        warehouse_id: { type: integer }
        status: { type: integer }
    PaymentMethod:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        sat_code: { type: string }
        status: { type: integer }
    CatalogItem:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        sat_code: { type: string }
        status: { type: integer }
    Stock:
      type: object
      properties:
        product_id: { type: integer }
        warehouse_id: { type: integer }
        qty: { type: number }
        lot: { type: string }
    Sale:
      type: object
      properties:
        id: { type: integer }
        folio: { type: string }
        date: { type: string }
        status: { type: string }
        customer_id: { type: integer }
        store_id: { type: integer }
        vendor_id: { type: integer }
        credit: { type: boolean }
        paid: { type: boolean }
        invoiced: { type: boolean }
        subtotal: { type: number }
        iva: { type: number }
        total: { type: number }
        lines:
          type: array
          items: { $ref: '#/components/schemas/SaleLine' }
        payments:
          type: array
          items: { $ref: '#/components/schemas/SalePayment' }
    SaleLine:
      type: object
      properties:
        id: { type: integer }
        product_id: { type: integer }
        qty: { type: number }
        price: { type: number }
        discount: { type: number }
        iva: { type: number }
    SalePayment:
      type: object
      properties:
        id: { type: integer }
        payment_method_id: { type: integer }
        amount: { type: number }
    Document:
      type: object
      properties:
        id: { type: integer }
        uuid: { type: string }
        series: { type: string }
        folio: { type: string }
        status: { type: string }
        stamped_at: { type: string }
        total: { type: number }
        sale_id: { type: integer }
        xml_url: { type: string }
        pdf_url: { type: string }
    SaleCreate:
      type: object
      required: [store_id, customer_id, lines]
      properties:
        store_id: { type: integer }
        customer_id: { type: integer }
        date: { type: string, format: date }
        credit: { type: boolean }
        notes: { type: string }
        lines:
          type: array
          items:
            type: object
            required: [product_id, qty, price]
            properties:
              product_id: { type: integer }
              qty: { type: number }
              price: { type: number, description: Precio unitario antes de IVA; debe coincidir con PrecioVenta del producto (integridad POS) }
              iva: { type: number }
              discount: { type: number }
        payments:
          type: array
          items:
            type: object
            required: [payment_method_id, amount]
            properties:
              payment_method_id: { type: integer }
              amount: { type: number }
  headers:
    Idempotency-Key:
      schema: { type: string }
