diff --git a/BUNDLES_API_DOCS.md b/BUNDLES_API_DOCS.md new file mode 100644 index 0000000..80b8cae --- /dev/null +++ b/BUNDLES_API_DOCS.md @@ -0,0 +1,815 @@ +# 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` diff --git a/src/pages/POS/Bundles/Create.vue b/src/pages/POS/Bundles/Create.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/POS/Bundles/Delete.vue b/src/pages/POS/Bundles/Delete.vue new file mode 100644 index 0000000..9aa63a8 --- /dev/null +++ b/src/pages/POS/Bundles/Delete.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/pages/POS/Bundles/Edit.vue b/src/pages/POS/Bundles/Edit.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/POS/Bundles/Index.vue b/src/pages/POS/Bundles/Index.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/POS/Bundles/Module.js b/src/pages/POS/Bundles/Module.js new file mode 100644 index 0000000..641d1c6 --- /dev/null +++ b/src/pages/POS/Bundles/Module.js @@ -0,0 +1,16 @@ +import { hasPermission } from '@Plugins/RolePermission.js'; + +// Ruta API +const apiTo = (name, params = {}) => route(`inventario.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => ({ name: `pos.inventory.${name}`, params, query }) + +// Determina si un usuario puede hacer algo en base a los permisos +const can = (permission) => hasPermission(`inventario.${permission}`) + +export { + can, + viewTo, + apiTo +} diff --git a/src/pages/POS/Bundles/ProductSelector.vue b/src/pages/POS/Bundles/ProductSelector.vue new file mode 100644 index 0000000..da9e375 --- /dev/null +++ b/src/pages/POS/Bundles/ProductSelector.vue @@ -0,0 +1,189 @@ + + +