# API de Bundles/Kits - Documentación Frontend ## 📋 Índice 1. [Conceptos Generales](#conceptos-generales) 2. [Estructura de Datos](#estructura-de-datos) 3. [Endpoints - CRUD de Bundles](#endpoints---crud-de-bundles) 4. [Endpoints - Ventas con Bundles](#endpoints---ventas-con-bundles) 5. [Validaciones](#validaciones) 6. [Ejemplos de Uso](#ejemplos-de-uso) 7. [Respuestas de Error](#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) ```json { "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 ```http 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:** ```json { "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 ```http GET /bundles/{id} Authorization: Bearer {token} ``` **Respuesta Exitosa:** ```json { "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 ```http POST /bundles Authorization: Bearer {token} Content-Type: application/json ``` **Request Body:** ```json { "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):** ```json { "model": { "id": 1, "name": "Kit Gamer Pro", "sku": "KIT-GAMER-001", ... }, "message": "Bundle creado exitosamente" } ``` --- ### 4. Actualizar Bundle ```http PUT /bundles/{id} Authorization: Bearer {token} Content-Type: application/json ``` **Request Body:** ```json { "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:** ```json { "model": {...}, "message": "Bundle actualizado exitosamente" } ``` --- ### 5. Eliminar Bundle ```http DELETE /bundles/{id} Authorization: Bearer {token} ``` **Respuesta Exitosa:** ```json { "message": "Bundle eliminado exitosamente" } ``` **Nota:** El bundle se elimina con soft delete (puede restaurarse). --- ### 6. Verificar Stock de Bundle ```http 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:** ```json { "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 ```http POST /sales Authorization: Bearer {token} Content-Type: application/json ``` **Request Body:** ```json { "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:** ```json { "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): ```json { "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: ```json { "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:** ```json { "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:** ```json { "message": "Stock insuficiente del kit 'Kit Gamer Pro'. Disponibles: 5, Requeridos: 10" } ``` **Ejemplo de error de serial:** ```json { "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** ```javascript 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** ```javascript 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** ```javascript 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 ```javascript 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 ```javascript 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 ```json { "message": "No query results for model [App\\Models\\Bundle] 1" } ``` #### 422 Validation Error ```json { "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 ```json { "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`