18 KiB
API de Bundles/Kits - Documentación Frontend
📋 Índice
- Conceptos Generales
- Estructura de Datos
- Endpoints - CRUD de Bundles
- Endpoints - Ventas con Bundles
- Validaciones
- Ejemplos de Uso
- 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 barraspage(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:
bundle_sale_group: Todos los componentes del mismo kit vendido comparten el mismo UUID- Precios Proporcionales: Los precios se distribuyen proporcionalmente del precio total del bundle
- Stock: Se decrementa automáticamente de cada componente
- 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_pricedel bundle (NO sumar componentes) - Calcular el tax basado en el
retail_pricedel 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_numbersporinventory_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_detailse puede devolver independientemente - El
bundle_sale_groupte 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