diff --git a/BUNDLES_API_DOCS.md b/BUNDLES_API_DOCS.md deleted file mode 100644 index 80b8cae..0000000 --- a/BUNDLES_API_DOCS.md +++ /dev/null @@ -1,815 +0,0 @@ -# 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/components/POS/CartItem.vue b/src/components/POS/CartItem.vue index ad49c4f..a809a21 100644 --- a/src/components/POS/CartItem.vue +++ b/src/components/POS/CartItem.vue @@ -64,7 +64,7 @@ const increment = () => { if (canIncrement.value) { const incrementValue = props.item.allows_decimals ? 1 : 1; const newQuantity = props.item.quantity + incrementValue; - emit('update-quantity', props.item.inventory_id, newQuantity); + emit('update-quantity', props.item.item_key, newQuantity); } }; @@ -74,9 +74,9 @@ const decrement = () => { if (props.item.quantity > minValue) { const newQuantity = Math.max(minValue, props.item.quantity - decrementValue); - emit('update-quantity', props.item.inventory_id, newQuantity); + emit('update-quantity', props.item.item_key, newQuantity); } else { - emit('remove', props.item.inventory_id); + emit('remove', props.item.item_key); } }; @@ -85,12 +85,12 @@ const handleQuantityInput = (event) => { const numValue = parseFloat(value); if (!isNaN(numValue) && numValue > 0) { - emit('update-quantity', props.item.inventory_id, numValue); + emit('update-quantity', props.item.item_key, numValue); } }; const remove = () => { - emit('remove', props.item.inventory_id); + emit('remove', props.item.item_key); }; @@ -98,9 +98,17 @@ const remove = () => {
@@ -463,15 +611,84 @@ onUnmounted(() => {
+ {{ searchQuery ? 'No se encontraron paquetes' : 'No hay paquetes disponibles' }} +
+