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

816 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`