816 lines
18 KiB
Markdown
816 lines
18 KiB
Markdown
# 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`
|