pdv.frontend/BUNDLES_API_DOCS.md
2026-02-16 17:16:13 -06:00

18 KiB
Raw Blame History

API de Bundles/Kits - Documentación Frontend

📋 Índice

  1. Conceptos Generales
  2. Estructura de Datos
  3. Endpoints - CRUD de Bundles
  4. Endpoints - Ventas con Bundles
  5. Validaciones
  6. Ejemplos de Uso
  7. Respuestas de Error

Conceptos Generales

¿Qué es un Bundle?

Un bundle (o kit) es una agrupación virtual de productos que se vende como una unidad con un precio especial. Al vender un bundle:

  • Se venden todos los componentes del bundle
  • Cada componente genera su propio sale_detail
  • El stock se decrementa de cada componente individual
  • Los seriales se asignan a cada componente que los requiera

Características Clave

Stock Virtual: El bundle NO tiene stock propio, se calcula desde sus componentes Precio Especial: El precio del bundle puede ser menor que la suma de componentes (promoción) Componentes Independientes: Los productos del bundle también pueden venderse por separado Devoluciones Flexibles: Se puede devolver el bundle completo o solo algunos componentes Rastreo de Seriales: Soporta componentes con números de serie


Estructura de Datos

Bundle (Kit)

{
  "id": 1,
  "name": "Kit Gamer Pro",
  "sku": "KIT-GAMER-001",
  "barcode": "123456789",
  "available_stock": 5,
  "total_cost": 250.00,
  "items": [
    {
      "id": 1,
      "bundle_id": 1,
      "inventory_id": 10,
      "quantity": 1,
      "inventory": {
        "id": 10,
        "name": "Mouse Gaming",
        "sku": "MOUSE-001",
        "stock": 50,
        "price": {
          "cost": 80.00,
          "retail_price": 150.00,
          "tax": 16.00
        }
      }
    },
    {
      "id": 2,
      "bundle_id": 1,
      "inventory_id": 15,
      "quantity": 1,
      "inventory": {
        "id": 15,
        "name": "Teclado Gaming",
        "sku": "TECLADO-001",
        "stock": 30,
        "price": {
          "cost": 120.00,
          "retail_price": 300.00,
          "tax": 16.00
        }
      }
    },
    {
      "id": 3,
      "bundle_id": 1,
      "inventory_id": 20,
      "quantity": 2,
      "inventory": {
        "id": 20,
        "name": "Mouse Pad",
        "sku": "PAD-001",
        "stock": 100,
        "price": {
          "cost": 25.00,
          "retail_price": 50.00,
          "tax": 16.00
        }
      }
    }
  ],
  "price": {
    "id": 1,
    "bundle_id": 1,
    "cost": 250.00,
    "retail_price": 450.00,
    "tax": 72.00
  },
  "created_at": "2026-02-16T10:00:00.000000Z",
  "updated_at": "2026-02-16T10:00:00.000000Z",
  "deleted_at": null
}

Campos Calculados

Campo Tipo Descripción
available_stock int Stock disponible del bundle = min(stock_componente / cantidad_requerida)
total_cost float Suma de costos de componentes = Σ(costo_componente × cantidad)

Ejemplo de cálculo de available_stock:

  • Mouse Gaming: stock 50 / cantidad 1 = 50 kits posibles
  • Teclado Gaming: stock 30 / cantidad 1 = 30 kits posibles
  • Mouse Pad: stock 100 / cantidad 2 = 50 kits posibles

Stock disponible = min(50, 30, 50) = 30 kits


Endpoints - CRUD de Bundles

1. Listar Bundles

GET /bundles?q={busqueda}&page={pagina}
Authorization: Bearer {token}

Query Parameters:

  • q (opcional): Búsqueda por nombre, SKU o código de barras
  • page (opcional): Número de página para paginación

Respuesta Exitosa:

{
  "bundles": {
    "data": [
      {
        "id": 1,
        "name": "Kit Gamer Pro",
        "sku": "KIT-GAMER-001",
        "available_stock": 30,
        "items": [...],
        "price": {...}
      }
    ],
    "current_page": 1,
    "last_page": 3,
    "per_page": 15,
    "total": 42
  }
}

2. Ver Detalle de Bundle

GET /bundles/{id}
Authorization: Bearer {token}

Respuesta Exitosa:

{
  "model": {
    "id": 1,
    "name": "Kit Gamer Pro",
    "sku": "KIT-GAMER-001",
    "barcode": "123456789",
    "available_stock": 30,
    "total_cost": 250.00,
    "items": [...],
    "price": {...}
  }
}

3. Crear Bundle

POST /bundles
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
  "name": "Kit Gamer Pro",
  "sku": "KIT-GAMER-001",
  "barcode": "123456789",
  "items": [
    {
      "inventory_id": 10,
      "quantity": 1
    },
    {
      "inventory_id": 15,
      "quantity": 1
    },
    {
      "inventory_id": 20,
      "quantity": 2
    }
  ],
  "retail_price": 450.00,
  "tax": 72.00
}

Campos:

Campo Tipo Requerido Descripción
name string Nombre del bundle (max 255 caracteres)
sku string SKU único del bundle (max 50 caracteres)
barcode string Código de barras (max 50 caracteres)
items array Componentes del bundle (mínimo 2)
items[].inventory_id int ID del producto componente
items[].quantity int Cantidad de este componente (min: 1)
retail_price float Precio de venta (si no se envía, se calcula automáticamente)
tax float Impuesto (si no se envía, se calcula como 16% del retail_price)

Respuesta Exitosa (201 Created):

{
  "model": {
    "id": 1,
    "name": "Kit Gamer Pro",
    "sku": "KIT-GAMER-001",
    ...
  },
  "message": "Bundle creado exitosamente"
}

4. Actualizar Bundle

PUT /bundles/{id}
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
  "name": "Kit Gamer Pro V2",
  "sku": "KIT-GAMER-002",
  "items": [
    {
      "inventory_id": 10,
      "quantity": 2
    },
    {
      "inventory_id": 15,
      "quantity": 1
    }
  ],
  "retail_price": 500.00,
  "recalculate_price": false
}

Campos:

Campo Tipo Requerido Descripción
name string Nuevo nombre del bundle
sku string Nuevo SKU (debe ser único)
items array Nuevos componentes (si se envía, reemplaza todos)
retail_price float Nuevo precio de venta
recalculate_price bool Si es true, recalcula precio desde componentes

Respuesta Exitosa:

{
  "model": {...},
  "message": "Bundle actualizado exitosamente"
}

5. Eliminar Bundle

DELETE /bundles/{id}
Authorization: Bearer {token}

Respuesta Exitosa:

{
  "message": "Bundle eliminado exitosamente"
}

Nota: El bundle se elimina con soft delete (puede restaurarse).


6. Verificar Stock de Bundle

GET /bundles/{id}/check-stock?quantity={cantidad}&warehouse_id={almacen}
Authorization: Bearer {token}

Query Parameters:

  • quantity (opcional): Cantidad de kits a verificar (default: 1)
  • warehouse_id (opcional): ID del almacén específico

Respuesta Exitosa:

{
  "bundle_id": 1,
  "bundle_name": "Kit Gamer Pro",
  "quantity_requested": 2,
  "available_stock": 30,
  "has_stock": true,
  "components_stock": [
    {
      "inventory_id": 10,
      "product_name": "Mouse Gaming",
      "required_quantity": 1,
      "available_stock": 50
    },
    {
      "inventory_id": 15,
      "product_name": "Teclado Gaming",
      "required_quantity": 1,
      "available_stock": 30
    },
    {
      "inventory_id": 20,
      "product_name": "Mouse Pad",
      "required_quantity": 2,
      "available_stock": 100
    }
  ]
}

Endpoints - Ventas con Bundles

Vender Bundle

POST /sales
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
  "user_id": 1,
  "client_id": 5,
  "subtotal": 450.00,
  "tax": 72.00,
  "total": 522.00,
  "payment_method": "cash",
  "cash_received": 600.00,
  "items": [
    {
      "type": "bundle",
      "bundle_id": 1,
      "quantity": 1,
      "warehouse_id": 1,
      "serial_numbers": {
        "15": ["SN-TECLADO-001"]
      }
    }
  ]
}

Campos del Item (Bundle):

Campo Tipo Requerido Descripción
type string Debe ser "bundle"
bundle_id int ID del bundle a vender
quantity int Cantidad de kits a vender
warehouse_id int Almacén de donde salen los componentes
serial_numbers object Seriales por componente (key = inventory_id, value = array de seriales)

Ejemplo de serial_numbers para múltiples componentes:

{
  "serial_numbers": {
    "10": ["SN-MOUSE-001"],
    "15": ["SN-TECLADO-001"],
    "20": ["SN-PAD-001", "SN-PAD-002"]
  }
}

Respuesta Exitosa:

La venta se crea con múltiples sale_details (uno por componente):

{
  "id": 100,
  "invoice_number": "INV-20260216-0001",
  "total": 522.00,
  "details": [
    {
      "id": 1,
      "sale_id": 100,
      "inventory_id": 10,
      "bundle_id": 1,
      "bundle_sale_group": "uuid-aaa-bbb-ccc",
      "product_name": "Mouse Gaming",
      "quantity": 1,
      "unit_price": 135.00,
      "subtotal": 135.00
    },
    {
      "id": 2,
      "sale_id": 100,
      "inventory_id": 15,
      "bundle_id": 1,
      "bundle_sale_group": "uuid-aaa-bbb-ccc",
      "product_name": "Teclado Gaming",
      "quantity": 1,
      "unit_price": 270.00,
      "subtotal": 270.00,
      "serials": [
        {
          "serial_number": "SN-TECLADO-001",
          "status": "vendido"
        }
      ]
    },
    {
      "id": 3,
      "sale_id": 100,
      "inventory_id": 20,
      "bundle_id": 1,
      "bundle_sale_group": "uuid-aaa-bbb-ccc",
      "product_name": "Mouse Pad",
      "quantity": 2,
      "unit_price": 45.00,
      "subtotal": 90.00
    }
  ]
}

Notas importantes:

  1. bundle_sale_group: Todos los componentes del mismo kit vendido comparten el mismo UUID
  2. Precios Proporcionales: Los precios se distribuyen proporcionalmente del precio total del bundle
  3. Stock: Se decrementa automáticamente de cada componente
  4. Seriales: Se asignan automáticamente a los componentes que usan track_serials=true

Venta Mixta (Productos + Bundles)

Puedes vender productos normales y bundles en la misma venta:

{
  "items": [
    {
      "type": "bundle",
      "bundle_id": 1,
      "quantity": 1
    },
    {
      "type": "product",
      "inventory_id": 50,
      "product_name": "Cable HDMI",
      "quantity": 2,
      "unit_price": 25.00,
      "subtotal": 50.00
    }
  ]
}

Si no se especifica type, se asume que es "product".


Validaciones

Validaciones de Bundle (Creación)

Campo Validaciones
name Requerido, string, máx 255 caracteres
sku Requerido, string, máx 50 caracteres, único en tabla bundles
barcode Opcional, string, máx 50 caracteres
items Requerido, array, mínimo 2 productos
items[].inventory_id Requerido, debe existir en tabla inventories
items[].quantity Requerido, entero, mínimo 1
retail_price Opcional, numérico, mínimo 0
tax Opcional, numérico, mínimo 0

Validaciones adicionales:

  • No se pueden agregar productos duplicados al mismo bundle
  • Cada producto solo puede aparecer UNA vez por bundle

Ejemplo de error:

{
  "message": "Error de validación",
  "errors": {
    "items": ["No se pueden agregar productos duplicados al bundle."],
    "sku": ["Este SKU ya está en uso."]
  }
}

Validaciones de Venta con Bundle

Campo Validaciones
type Opcional, debe ser "product" o "bundle"
bundle_id Requerido si type="bundle", debe existir en tabla bundles
quantity Requerido, entero, mínimo 1
warehouse_id Opcional, debe existir en tabla warehouses
serial_numbers Opcional, objeto con arrays de seriales

Validaciones de stock:

  • Se valida que TODOS los componentes tengan stock suficiente
  • Se valida que los seriales existan y estén disponibles
  • Si falta stock de UN componente, la venta completa falla

Ejemplo de error de stock:

{
  "message": "Stock insuficiente del kit 'Kit Gamer Pro'. Disponibles: 5, Requeridos: 10"
}

Ejemplo de error de serial:

{
  "message": "Serial SN-TECLADO-999 no disponible para Teclado Gaming"
}

Ejemplos de Uso

Ejemplo 1: Flujo Completo - Crear y Vender Bundle

Paso 1: Crear Bundle

const response = await fetch('/bundles', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: "Kit Gamer Pro",
    sku: "KIT-GAMER-001",
    items: [
      { inventory_id: 10, quantity: 1 }, // Mouse
      { inventory_id: 15, quantity: 1 }, // Teclado
      { inventory_id: 20, quantity: 2 }  // Mouse Pads
    ],
    retail_price: 450.00,
    tax: 72.00
  })
});

const bundle = await response.json();
console.log("Bundle creado:", bundle.model.id);

Paso 2: Verificar Stock

const stockResponse = await fetch(`/bundles/${bundle.model.id}/check-stock?quantity=5`, {
  headers: { 'Authorization': `Bearer ${token}` }
});

const stockData = await stockResponse.json();

if (stockData.has_stock) {
  console.log(`Stock disponible: ${stockData.available_stock} kits`);
} else {
  console.log("Stock insuficiente");
  console.table(stockData.components_stock);
}

Paso 3: Vender Bundle

const saleResponse = await fetch('/sales', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    user_id: 1,
    subtotal: 450.00,
    tax: 72.00,
    total: 522.00,
    payment_method: "cash",
    cash_received: 600.00,
    items: [
      {
        type: "bundle",
        bundle_id: bundle.model.id,
        quantity: 1,
        warehouse_id: 1
      }
    ]
  })
});

const sale = await saleResponse.json();
console.log(`Venta creada: ${sale.invoice_number}`);
console.log(`Total de componentes vendidos: ${sale.details.length}`);

Ejemplo 2: Buscar Bundle por SKU

const searchResponse = await fetch('/bundles?q=GAMER', {
  headers: { 'Authorization': `Bearer ${token}` }
});

const { bundles } = await searchResponse.json();

bundles.data.forEach(bundle => {
  console.log(`${bundle.name} - Stock: ${bundle.available_stock}`);
});

Ejemplo 3: Actualizar Precio de Bundle

await fetch(`/bundles/${bundleId}`, {
  method: 'PUT',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    retail_price: 399.99, // Nuevo precio promocional
    tax: 64.00
  })
});

Respuestas de Error

Errores Comunes

404 Not Found

{
  "message": "No query results for model [App\\Models\\Bundle] 1"
}

422 Validation Error

{
  "message": "Error de validación",
  "errors": {
    "items": ["Un bundle debe tener al menos 2 productos."],
    "sku": ["Este SKU ya está en uso."]
  }
}

500 Internal Server Error

{
  "message": "Error al crear el bundle: Stock insuficiente del kit 'Kit Gamer Pro'. Disponibles: 5, Requeridos: 10"
}

Notas Importantes para Frontend

1. Cálculo de Totales

Cuando vendes un bundle, el frontend debe:

  • Usar el retail_price del bundle (NO sumar componentes)
  • Calcular el tax basado en el retail_price del bundle
  • El backend distribuirá los precios proporcionalmente a los componentes

2. Visualización de Stock

El available_stock ya está calculado por el backend. No necesitas:

  • Sumar el stock de componentes
  • Hacer cálculos manuales

Solo muestra el valor de available_stock directamente.

3. Seriales en Bundles

Si un componente del bundle usa seriales (track_serials=true):

  • Puedes especificarlos en serial_numbers por inventory_id
  • Si no los especificas, se asignan automáticamente
  • Solo necesitas seriales para componentes que los requieran

4. Devoluciones

Las devoluciones de bundles funcionan igual que productos normales:

  • Puedes devolver componentes individuales
  • Cada sale_detail se puede devolver independientemente
  • El bundle_sale_group te ayuda a identificar qué componentes pertenecen al mismo kit vendido

5. Cancelación de Ventas

La cancelación funciona automáticamente:

  • El stock de TODOS los componentes se restaura
  • Los seriales vuelven a estado disponible
  • No necesitas lógica especial en el frontend

Cambios Respecto a Ventas Normales

Aspecto Venta Normal Venta de Bundle
Items type: "product" o sin type type: "bundle"
ID del producto inventory_id bundle_id
Sale Details 1 por item N por bundle (N = componentes)
Stock Decrementa 1 producto Decrementa N componentes
Seriales Por producto Por componente (si requiere)
Devolución Por producto Por componente

Diagrama de Relaciones

Bundle (Kit)
  ├── BundlePrice (1:1)
  │     └── cost, retail_price, tax
  │
  └── BundleItems (1:N)
        └── Inventory (N:1)
              ├── Stock (en inventory_warehouse)
              ├── Price
              └── Serials (si track_serials=true)

Sale
  └── SaleDetails (1:N)
        ├── bundle_id (FK, nullable)
        ├── bundle_sale_group (UUID, nullable)
        └── Inventory (N:1)

Soporte

Si encuentras algún problema o tienes dudas sobre la implementación, contacta al equipo de backend o consulta el código fuente en:

  • Controlador: app/Http/Controllers/App/BundleController.php
  • Servicio: app/Services/BundleService.php
  • Modelos: app/Models/Bundle.php, app/Models/BundleItem.php, app/Models/BundlePrice.php
  • Plan completo: ~/.claude/plans/fancy-roaming-shamir.md