feat: implementar gestión de bundles, incluyendo creación, actualización y eliminación, así como validación de stock
This commit is contained in:
parent
7ebca6456f
commit
7a68c458b8
140
app/Http/Controllers/App/BundleController.php
Normal file
140
app/Http/Controllers/App/BundleController.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php namespace App\Http\Controllers\App;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Bundle;
|
||||||
|
use App\Services\BundleService;
|
||||||
|
use App\Http\Requests\App\BundleStoreRequest;
|
||||||
|
use App\Http\Requests\App\BundleUpdateRequest;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
class BundleController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected BundleService $bundleService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listar todos los bundles activos
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$bundles = Bundle::with(['items.inventory.price', 'price'])
|
||||||
|
->when($request->has('q'), function ($query) use ($request) {
|
||||||
|
$query->where(function ($q) use ($request) {
|
||||||
|
$q->where('name', 'like', "%{$request->q}%")
|
||||||
|
->orWhere('sku', 'like', "%{$request->q}%")
|
||||||
|
->orWhere('barcode', $request->q);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('name')
|
||||||
|
->paginate(config('app.pagination', 15));
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'bundles' => $bundles,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ver detalle de un bundle
|
||||||
|
*/
|
||||||
|
public function show(Bundle $bundle)
|
||||||
|
{
|
||||||
|
$bundle->load(['items.inventory.price', 'price']);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'model' => $bundle,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear un nuevo bundle
|
||||||
|
*/
|
||||||
|
public function store(BundleStoreRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$bundle = $this->bundleService->createBundle($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'model' => $bundle,
|
||||||
|
'message' => 'Bundle creado exitosamente',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al crear el bundle: ' . $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar un bundle existente
|
||||||
|
*/
|
||||||
|
public function update(BundleUpdateRequest $request, Bundle $bundle)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$updatedBundle = $this->bundleService->updateBundle($bundle, $request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'model' => $updatedBundle,
|
||||||
|
'message' => 'Bundle actualizado exitosamente',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al actualizar el bundle: ' . $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar (soft delete) un bundle
|
||||||
|
*/
|
||||||
|
public function destroy(Bundle $bundle)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->bundleService->deleteBundle($bundle);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Bundle eliminado exitosamente',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al eliminar el bundle: ' . $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar stock disponible de un bundle
|
||||||
|
*/
|
||||||
|
public function checkStock(Request $request, Bundle $bundle)
|
||||||
|
{
|
||||||
|
$quantity = $request->input('quantity', 1);
|
||||||
|
$warehouseId = $request->input('warehouse_id');
|
||||||
|
|
||||||
|
$bundle->load(['items.inventory']);
|
||||||
|
|
||||||
|
$availableStock = $warehouseId
|
||||||
|
? $bundle->stockInWarehouse($warehouseId)
|
||||||
|
: $bundle->available_stock;
|
||||||
|
|
||||||
|
$hasStock = $bundle->hasStock($quantity, $warehouseId);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'bundle_id' => $bundle->id,
|
||||||
|
'bundle_name' => $bundle->name,
|
||||||
|
'quantity_requested' => $quantity,
|
||||||
|
'available_stock' => $availableStock,
|
||||||
|
'has_stock' => $hasStock,
|
||||||
|
'components_stock' => $bundle->items->map(function ($item) use ($warehouseId) {
|
||||||
|
return [
|
||||||
|
'inventory_id' => $item->inventory_id,
|
||||||
|
'product_name' => $item->inventory->name,
|
||||||
|
'required_quantity' => $item->quantity,
|
||||||
|
'available_stock' => $warehouseId
|
||||||
|
? $item->inventory->stockInWarehouse($warehouseId)
|
||||||
|
: $item->inventory->stock,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Http/Requests/App/BundleStoreRequest.php
Normal file
73
app/Http/Requests/App/BundleStoreRequest.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\App;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BundleStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'sku' => ['required', 'string', 'max:50', 'unique:bundles,sku'],
|
||||||
|
'barcode' => ['nullable', 'string', 'max:50'],
|
||||||
|
|
||||||
|
// Componentes del kit (mínimo 2 productos)
|
||||||
|
'items' => ['required', 'array', 'min:2'],
|
||||||
|
'items.*.inventory_id' => ['required', 'exists:inventories,id'],
|
||||||
|
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||||
|
|
||||||
|
// Precio (opcional, se calcula automáticamente si no se provee)
|
||||||
|
'retail_price' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'tax' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'El nombre del bundle es obligatorio.',
|
||||||
|
'sku.required' => 'El SKU es obligatorio.',
|
||||||
|
'sku.unique' => 'Este SKU ya está en uso.',
|
||||||
|
'items.required' => 'Debes agregar productos al bundle.',
|
||||||
|
'items.min' => 'Un bundle debe tener al menos 2 productos.',
|
||||||
|
'items.*.inventory_id.required' => 'Cada producto debe tener un ID válido.',
|
||||||
|
'items.*.inventory_id.exists' => 'Uno de los productos no existe.',
|
||||||
|
'items.*.quantity.required' => 'La cantidad es obligatoria.',
|
||||||
|
'items.*.quantity.min' => 'La cantidad debe ser al menos 1.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validación adicional
|
||||||
|
*/
|
||||||
|
public function withValidator($validator)
|
||||||
|
{
|
||||||
|
$validator->after(function ($validator) {
|
||||||
|
// Validar que no haya productos duplicados
|
||||||
|
$inventoryIds = collect($this->items)->pluck('inventory_id')->toArray();
|
||||||
|
|
||||||
|
if (count($inventoryIds) !== count(array_unique($inventoryIds))) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'items',
|
||||||
|
'No se pueden agregar productos duplicados al bundle.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Http/Requests/App/BundleUpdateRequest.php
Normal file
73
app/Http/Requests/App/BundleUpdateRequest.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\App;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BundleUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$bundleId = $this->route('bundle')?->id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => ['nullable', 'string', 'max:255'],
|
||||||
|
'sku' => ['nullable', 'string', 'max:50', 'unique:bundles,sku,' . $bundleId],
|
||||||
|
'barcode' => ['nullable', 'string', 'max:50'],
|
||||||
|
|
||||||
|
// Componentes del kit (opcional en update)
|
||||||
|
'items' => ['nullable', 'array', 'min:2'],
|
||||||
|
'items.*.inventory_id' => ['required_with:items', 'exists:inventories,id'],
|
||||||
|
'items.*.quantity' => ['required_with:items', 'integer', 'min:1'],
|
||||||
|
|
||||||
|
// Precio
|
||||||
|
'retail_price' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'tax' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'recalculate_price' => ['nullable', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sku.unique' => 'Este SKU ya está en uso.',
|
||||||
|
'items.min' => 'Un bundle debe tener al menos 2 productos.',
|
||||||
|
'items.*.inventory_id.exists' => 'Uno de los productos no existe.',
|
||||||
|
'items.*.quantity.min' => 'La cantidad debe ser al menos 1.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validación adicional
|
||||||
|
*/
|
||||||
|
public function withValidator($validator)
|
||||||
|
{
|
||||||
|
$validator->after(function ($validator) {
|
||||||
|
// Validar que no haya productos duplicados (si se proporcionan items)
|
||||||
|
if ($this->has('items')) {
|
||||||
|
$inventoryIds = collect($this->items)->pluck('inventory_id')->toArray();
|
||||||
|
|
||||||
|
if (count($inventoryIds) !== count(array_unique($inventoryIds))) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'items',
|
||||||
|
'No se pueden agregar productos duplicados al bundle.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,13 +34,24 @@ public function rules(): array
|
|||||||
|
|
||||||
// Items del carrito
|
// Items del carrito
|
||||||
'items' => ['required', 'array', 'min:1'],
|
'items' => ['required', 'array', 'min:1'],
|
||||||
'items.*.inventory_id' => ['required', 'exists:inventories,id'],
|
|
||||||
'items.*.product_name' => ['required', 'string', 'max:255'],
|
// Items pueden ser productos O bundles
|
||||||
|
'items.*.type' => ['nullable', 'in:product,bundle'],
|
||||||
|
'items.*.bundle_id' => ['required_if:items.*.type,bundle', 'exists:bundles,id'],
|
||||||
|
|
||||||
|
// Para productos normales
|
||||||
|
'items.*.inventory_id' => ['required_if:items.*.type,product', 'exists:inventories,id'],
|
||||||
|
'items.*.product_name' => ['required_if:items.*.type,product', 'string', 'max:255'],
|
||||||
|
'items.*.unit_price' => ['required_if:items.*.type,product', 'numeric', 'min:0'],
|
||||||
|
'items.*.subtotal' => ['required_if:items.*.type,product', 'numeric', 'min:0'],
|
||||||
|
|
||||||
|
// Comunes a ambos
|
||||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||||
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
|
'items.*.warehouse_id' => ['nullable', 'exists:warehouses,id'],
|
||||||
'items.*.subtotal' => ['required', 'numeric', 'min:0'],
|
|
||||||
|
// Seriales (para productos normales o componentes del bundle)
|
||||||
'items.*.serial_numbers' => ['nullable', 'array'],
|
'items.*.serial_numbers' => ['nullable', 'array'],
|
||||||
'items.*.serial_numbers.*' => ['string', 'exists:inventory_serials,serial_number'],
|
'items.*.serial_numbers.*' => ['nullable', 'array'], // Para bundles: {inventory_id: [serials]}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
115
app/Models/Bundle.php
Normal file
115
app/Models/Bundle.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Bundle extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'sku',
|
||||||
|
'barcode',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = ['available_stock', 'total_cost'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Componentes del kit (items individuales)
|
||||||
|
*/
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(BundleItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Productos que componen el kit (relación many-to-many)
|
||||||
|
*/
|
||||||
|
public function inventories()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Inventory::class, 'bundle_items')
|
||||||
|
->withPivot('quantity')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precio del kit
|
||||||
|
*/
|
||||||
|
public function price()
|
||||||
|
{
|
||||||
|
return $this->hasOne(BundlePrice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stock disponible del kit = mínimo(stock_componente / cantidad_requerida)
|
||||||
|
*/
|
||||||
|
public function getAvailableStockAttribute(): int
|
||||||
|
{
|
||||||
|
if ($this->items->isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$minStock = PHP_INT_MAX;
|
||||||
|
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$inventory = $item->inventory;
|
||||||
|
$availableStock = $inventory->stock;
|
||||||
|
|
||||||
|
// Cuántos kits puedo hacer con este componente
|
||||||
|
$possibleKits = $availableStock > 0 ? floor($availableStock / $item->quantity) : 0;
|
||||||
|
$minStock = min($minStock, $possibleKits);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $minStock === PHP_INT_MAX ? 0 : (int) $minStock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stock disponible en un almacén específico
|
||||||
|
*/
|
||||||
|
public function stockInWarehouse(int $warehouseId): int
|
||||||
|
{
|
||||||
|
if ($this->items->isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$minStock = PHP_INT_MAX;
|
||||||
|
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$inventory = $item->inventory;
|
||||||
|
$warehouseStock = $inventory->stockInWarehouse($warehouseId);
|
||||||
|
|
||||||
|
$possibleKits = $warehouseStock > 0 ? floor($warehouseStock / $item->quantity) : 0;
|
||||||
|
$minStock = min($minStock, $possibleKits);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $minStock === PHP_INT_MAX ? 0 : (int) $minStock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Costo total del kit (suma de costos de componentes)
|
||||||
|
*/
|
||||||
|
public function getTotalCostAttribute(): float
|
||||||
|
{
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$componentCost = $item->inventory->price->cost ?? 0;
|
||||||
|
$total += $componentCost * $item->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($total, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validar si el kit tiene stock disponible
|
||||||
|
*/
|
||||||
|
public function hasStock(int $quantity = 1, ?int $warehouseId = null): bool
|
||||||
|
{
|
||||||
|
if ($warehouseId) {
|
||||||
|
return $this->stockInWarehouse($warehouseId) >= $quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->available_stock >= $quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Models/BundleItem.php
Normal file
32
app/Models/BundleItem.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class BundleItem extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'bundle_id',
|
||||||
|
'inventory_id',
|
||||||
|
'quantity',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'quantity' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle al que pertenece este item
|
||||||
|
*/
|
||||||
|
public function bundle()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Bundle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Producto componente
|
||||||
|
*/
|
||||||
|
public function inventory()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Inventory::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Models/BundlePrice.php
Normal file
27
app/Models/BundlePrice.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class BundlePrice extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'bundle_id',
|
||||||
|
'cost',
|
||||||
|
'retail_price',
|
||||||
|
'tax',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'cost' => 'decimal:2',
|
||||||
|
'retail_price' => 'decimal:2',
|
||||||
|
'tax' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle al que pertenece este precio
|
||||||
|
*/
|
||||||
|
public function bundle()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Bundle::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,8 @@ class SaleDetail extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'sale_id',
|
'sale_id',
|
||||||
'inventory_id',
|
'inventory_id',
|
||||||
|
'bundle_id',
|
||||||
|
'bundle_sale_group',
|
||||||
'warehouse_id',
|
'warehouse_id',
|
||||||
'product_name',
|
'product_name',
|
||||||
'quantity',
|
'quantity',
|
||||||
@ -101,4 +103,35 @@ public function getMaxReturnableQuantityAttribute(): int
|
|||||||
{
|
{
|
||||||
return $this->getQuantityRemainingAttribute();
|
return $this->getQuantityRemainingAttribute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle al que pertenece este sale_detail (si es componente de un kit)
|
||||||
|
*/
|
||||||
|
public function bundle()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Bundle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si este sale_detail es parte de un kit
|
||||||
|
*/
|
||||||
|
public function isPartOfBundle(): bool
|
||||||
|
{
|
||||||
|
return !is_null($this->bundle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener todos los sale_details del mismo kit vendido
|
||||||
|
* (todos los componentes con el mismo bundle_sale_group)
|
||||||
|
*/
|
||||||
|
public function bundleComponents()
|
||||||
|
{
|
||||||
|
if (!$this->isPartOfBundle()) {
|
||||||
|
return collect([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SaleDetail::where('sale_id', $this->sale_id)
|
||||||
|
->where('bundle_sale_group', $this->bundle_sale_group)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
175
app/Services/BundleService.php
Normal file
175
app/Services/BundleService.php
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Bundle;
|
||||||
|
use App\Models\BundleItem;
|
||||||
|
use App\Models\BundlePrice;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class BundleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Crear un nuevo kit/bundle con sus componentes y precio
|
||||||
|
*/
|
||||||
|
public function createBundle(array $data): Bundle
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
// 1. Crear el bundle principal
|
||||||
|
$bundle = Bundle::create([
|
||||||
|
'name' => $data['name'],
|
||||||
|
'sku' => $data['sku'],
|
||||||
|
'barcode' => $data['barcode'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Agregar componentes al kit
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
BundleItem::create([
|
||||||
|
'bundle_id' => $bundle->id,
|
||||||
|
'inventory_id' => $item['inventory_id'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Calcular costos y crear precio
|
||||||
|
$bundle->load('items.inventory.price');
|
||||||
|
|
||||||
|
$totalCost = 0;
|
||||||
|
$totalRetailPrice = 0;
|
||||||
|
|
||||||
|
foreach ($bundle->items as $item) {
|
||||||
|
$totalCost += ($item->inventory->price->cost ?? 0) * $item->quantity;
|
||||||
|
$totalRetailPrice += ($item->inventory->price->retail_price ?? 0) * $item->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permitir override de precio (para promociones)
|
||||||
|
$finalRetailPrice = $data['retail_price'] ?? $totalRetailPrice;
|
||||||
|
$tax = $data['tax'] ?? ($finalRetailPrice * 0.16); // 16% por defecto
|
||||||
|
|
||||||
|
BundlePrice::create([
|
||||||
|
'bundle_id' => $bundle->id,
|
||||||
|
'cost' => $totalCost,
|
||||||
|
'retail_price' => $finalRetailPrice,
|
||||||
|
'tax' => $tax,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $bundle->fresh(['items.inventory.price', 'price']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar un kit existente
|
||||||
|
*/
|
||||||
|
public function updateBundle(Bundle $bundle, array $data): Bundle
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($bundle, $data) {
|
||||||
|
// 1. Actualizar datos básicos del bundle
|
||||||
|
$bundle->update([
|
||||||
|
'name' => $data['name'] ?? $bundle->name,
|
||||||
|
'sku' => $data['sku'] ?? $bundle->sku,
|
||||||
|
'barcode' => $data['barcode'] ?? $bundle->barcode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Actualizar componentes si se proporcionan
|
||||||
|
if (isset($data['items'])) {
|
||||||
|
// Eliminar componentes actuales
|
||||||
|
$bundle->items()->delete();
|
||||||
|
|
||||||
|
// Crear nuevos componentes
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
BundleItem::create([
|
||||||
|
'bundle_id' => $bundle->id,
|
||||||
|
'inventory_id' => $item['inventory_id'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Recalcular o actualizar precio
|
||||||
|
if (isset($data['recalculate_price']) && $data['recalculate_price']) {
|
||||||
|
// Recalcular precio basado en componentes actuales
|
||||||
|
$bundle->load('items.inventory.price');
|
||||||
|
|
||||||
|
$totalCost = 0;
|
||||||
|
$totalRetailPrice = 0;
|
||||||
|
|
||||||
|
foreach ($bundle->items as $item) {
|
||||||
|
$totalCost += ($item->inventory->price->cost ?? 0) * $item->quantity;
|
||||||
|
$totalRetailPrice += ($item->inventory->price->retail_price ?? 0) * $item->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalRetailPrice = $data['retail_price'] ?? $totalRetailPrice;
|
||||||
|
$tax = $data['tax'] ?? ($finalRetailPrice * 0.16);
|
||||||
|
|
||||||
|
$bundle->price->update([
|
||||||
|
'cost' => $totalCost,
|
||||||
|
'retail_price' => $finalRetailPrice,
|
||||||
|
'tax' => $tax,
|
||||||
|
]);
|
||||||
|
} elseif (isset($data['retail_price'])) {
|
||||||
|
// Solo actualizar precio sin recalcular componentes
|
||||||
|
$bundle->price->update([
|
||||||
|
'retail_price' => $data['retail_price'],
|
||||||
|
'tax' => $data['tax'] ?? ($data['retail_price'] * 0.16),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bundle->fresh(['items.inventory.price', 'price']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validar disponibilidad de stock del kit
|
||||||
|
*/
|
||||||
|
public function validateBundleAvailability(Bundle $bundle, int $quantity, ?int $warehouseId = null): void
|
||||||
|
{
|
||||||
|
if (!$bundle->hasStock($quantity, $warehouseId)) {
|
||||||
|
$availableStock = $warehouseId
|
||||||
|
? $bundle->stockInWarehouse($warehouseId)
|
||||||
|
: $bundle->available_stock;
|
||||||
|
|
||||||
|
throw new \Exception(
|
||||||
|
"Stock insuficiente del kit '{$bundle->name}'. " .
|
||||||
|
"Disponibles: {$availableStock}, Requeridos: {$quantity}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener precio total de componentes (sin promoción)
|
||||||
|
*/
|
||||||
|
public function getComponentsValue(Bundle $bundle): float
|
||||||
|
{
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
foreach ($bundle->items as $item) {
|
||||||
|
$componentPrice = $item->inventory->price->retail_price ?? 0;
|
||||||
|
$total += $componentPrice * $item->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($total, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar (soft delete) un bundle
|
||||||
|
*/
|
||||||
|
public function deleteBundle(Bundle $bundle): bool
|
||||||
|
{
|
||||||
|
return $bundle->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaurar un bundle eliminado
|
||||||
|
*/
|
||||||
|
public function restoreBundle(int $bundleId): ?Bundle
|
||||||
|
{
|
||||||
|
$bundle = Bundle::withTrashed()->find($bundleId);
|
||||||
|
|
||||||
|
if ($bundle && $bundle->trashed()) {
|
||||||
|
$bundle->restore();
|
||||||
|
return $bundle->fresh(['items.inventory.price', 'price']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<?php namespace App\Services;
|
<?php namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Bundle;
|
||||||
use App\Models\CashRegister;
|
use App\Models\CashRegister;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Sale;
|
use App\Models\Sale;
|
||||||
@ -7,6 +8,7 @@
|
|||||||
use App\Models\Inventory;
|
use App\Models\Inventory;
|
||||||
use App\Models\InventorySerial;
|
use App\Models\InventorySerial;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class SaleService
|
class SaleService
|
||||||
{
|
{
|
||||||
@ -72,8 +74,14 @@ public function createSale(array $data)
|
|||||||
'status' => $data['status'] ?? 'completed',
|
'status' => $data['status'] ?? 'completed',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. Crear los detalles de la venta y asignar seriales
|
// 2. Expandir bundles en componentes individuales
|
||||||
foreach ($data['items'] as $item) {
|
$expandedItems = $this->expandBundlesIntoComponents($data['items']);
|
||||||
|
|
||||||
|
// 2.1. Validar stock de TODOS los items (componentes + productos normales)
|
||||||
|
$this->validateStockForAllItems($expandedItems);
|
||||||
|
|
||||||
|
// 3. Crear los detalles de la venta y asignar seriales
|
||||||
|
foreach ($expandedItems as $item) {
|
||||||
// Calcular descuento por detalle si aplica
|
// Calcular descuento por detalle si aplica
|
||||||
$itemDiscountAmount = 0;
|
$itemDiscountAmount = 0;
|
||||||
if ($discountPercentage > 0) {
|
if ($discountPercentage > 0) {
|
||||||
@ -84,6 +92,9 @@ public function createSale(array $data)
|
|||||||
$saleDetail = SaleDetail::create([
|
$saleDetail = SaleDetail::create([
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'inventory_id' => $item['inventory_id'],
|
'inventory_id' => $item['inventory_id'],
|
||||||
|
'bundle_id' => $item['bundle_id'] ?? null,
|
||||||
|
'bundle_sale_group' => $item['bundle_sale_group'] ?? null,
|
||||||
|
'warehouse_id' => $item['warehouse_id'] ?? null,
|
||||||
'product_name' => $item['product_name'],
|
'product_name' => $item['product_name'],
|
||||||
'quantity' => $item['quantity'],
|
'quantity' => $item['quantity'],
|
||||||
'unit_price' => $item['unit_price'],
|
'unit_price' => $item['unit_price'],
|
||||||
@ -244,4 +255,113 @@ private function getCurrentCashRegister($userId)
|
|||||||
|
|
||||||
return $register ? $register->id : null;
|
return $register ? $register->id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expandir bundles en componentes individuales
|
||||||
|
*/
|
||||||
|
private function expandBundlesIntoComponents(array $items): array
|
||||||
|
{
|
||||||
|
$expanded = [];
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
// Detectar si es un bundle
|
||||||
|
if (isset($item['type']) && $item['type'] === 'bundle') {
|
||||||
|
// Es un kit, expandir en componentes
|
||||||
|
$bundle = Bundle::with(['items.inventory.price', 'price'])->findOrFail($item['bundle_id']);
|
||||||
|
$bundleQuantity = $item['quantity'];
|
||||||
|
$bundleSaleGroup = Str::uuid()->toString();
|
||||||
|
|
||||||
|
// Calcular precio por unidad de kit (para distribuir)
|
||||||
|
$bundleTotalPrice = $bundle->price->retail_price;
|
||||||
|
$bundleComponentsValue = $this->calculateBundleComponentsValue($bundle);
|
||||||
|
|
||||||
|
foreach ($bundle->items as $bundleItem) {
|
||||||
|
$componentInventory = $bundleItem->inventory;
|
||||||
|
$componentQuantity = $bundleItem->quantity * $bundleQuantity;
|
||||||
|
|
||||||
|
// Calcular precio proporcional del componente
|
||||||
|
$componentValue = ($componentInventory->price->retail_price ?? 0) * $bundleItem->quantity;
|
||||||
|
$priceRatio = $bundleComponentsValue > 0 ? $componentValue / $bundleComponentsValue : 0;
|
||||||
|
$componentUnitPrice = round($bundleTotalPrice * $priceRatio / $bundleItem->quantity, 2);
|
||||||
|
|
||||||
|
$expanded[] = [
|
||||||
|
'inventory_id' => $componentInventory->id,
|
||||||
|
'bundle_id' => $bundle->id,
|
||||||
|
'bundle_sale_group' => $bundleSaleGroup,
|
||||||
|
'warehouse_id' => $item['warehouse_id'] ?? null,
|
||||||
|
'product_name' => $componentInventory->name,
|
||||||
|
'quantity' => $componentQuantity,
|
||||||
|
'unit_price' => $componentUnitPrice,
|
||||||
|
'subtotal' => $componentUnitPrice * $componentQuantity,
|
||||||
|
'serial_numbers' => $item['serial_numbers'][$componentInventory->id] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Producto normal, agregar tal cual
|
||||||
|
$expanded[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validar stock de todos los items (antes de crear sale_details)
|
||||||
|
*/
|
||||||
|
private function validateStockForAllItems(array $items): void
|
||||||
|
{
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$inventory = Inventory::find($item['inventory_id']);
|
||||||
|
$warehouseId = $item['warehouse_id'] ?? $this->movementService->getMainWarehouseId();
|
||||||
|
|
||||||
|
if ($inventory->track_serials) {
|
||||||
|
// Validar seriales disponibles
|
||||||
|
if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) {
|
||||||
|
// Validar que los seriales específicos existan y estén disponibles
|
||||||
|
foreach ($item['serial_numbers'] as $serialNumber) {
|
||||||
|
$serial = InventorySerial::where('inventory_id', $inventory->id)
|
||||||
|
->where('serial_number', $serialNumber)
|
||||||
|
->where('status', 'disponible')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$serial) {
|
||||||
|
throw new \Exception(
|
||||||
|
"Serial {$serialNumber} no disponible para {$inventory->name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validar que haya suficientes seriales disponibles
|
||||||
|
$availableSerials = InventorySerial::where('inventory_id', $inventory->id)
|
||||||
|
->where('status', 'disponible')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($availableSerials < $item['quantity']) {
|
||||||
|
throw new \Exception(
|
||||||
|
"Stock insuficiente de seriales para {$inventory->name}. " .
|
||||||
|
"Disponibles: {$availableSerials}, Requeridos: {$item['quantity']}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validar stock en almacén
|
||||||
|
$this->movementService->validateStock($inventory->id, $warehouseId, $item['quantity']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener precio total de componentes de un bundle
|
||||||
|
*/
|
||||||
|
private function calculateBundleComponentsValue(Bundle $bundle): float
|
||||||
|
{
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
foreach ($bundle->items as $item) {
|
||||||
|
$componentPrice = $item->inventory->price->retail_price ?? 0;
|
||||||
|
$total += $componentPrice * $item->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($total, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,26 +158,6 @@ public function sendInvoice(
|
|||||||
return $pdfResult;
|
return $pdfResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enviar ticket de venta por WhatsApp
|
|
||||||
*/
|
|
||||||
public function sendSaleTicket(
|
|
||||||
string $phoneNumber,
|
|
||||||
string $ticketUrl,
|
|
||||||
string $saleNumber,
|
|
||||||
string $customerName
|
|
||||||
): array {
|
|
||||||
return $this->sendDocument(
|
|
||||||
phoneNumber: $phoneNumber,
|
|
||||||
documentUrl: $ticketUrl,
|
|
||||||
caption: "Ticket de venta {$saleNumber} - {$customerName}. Gracias por su compra.",
|
|
||||||
filename: "Ticket_{$saleNumber}.pdf",
|
|
||||||
userEmail: $this->email,
|
|
||||||
ticket: $saleNumber,
|
|
||||||
customerName: $customerName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpiar número de teléfono
|
* Limpiar número de teléfono
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('bundles', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('sku')->unique();
|
||||||
|
$table->string('barcode')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('bundles');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('bundle_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('bundle_id')->constrained('bundles')->onDelete('cascade');
|
||||||
|
$table->foreignId('inventory_id')->constrained('inventories')->onDelete('restrict');
|
||||||
|
$table->integer('quantity')->default(1);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Un producto solo puede aparecer una vez por kit
|
||||||
|
$table->unique(['bundle_id', 'inventory_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('bundle_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('bundle_prices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('bundle_id')->unique()->constrained('bundles')->onDelete('cascade');
|
||||||
|
$table->decimal('cost', 10, 2);
|
||||||
|
$table->decimal('retail_price', 10, 2);
|
||||||
|
$table->decimal('tax', 10, 2);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('bundle_prices');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('sale_details', function (Blueprint $table) {
|
||||||
|
|
||||||
|
$table->foreignId('bundle_id')->nullable()->after('inventory_id')->constrained('bundles')->onDelete('restrict');
|
||||||
|
$table->string('bundle_sale_group', 36)->nullable()->after('bundle_id');
|
||||||
|
$table->index('bundle_id');
|
||||||
|
$table->index('bundle_sale_group');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('sale_details', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['bundle_id']);
|
||||||
|
$table->dropIndex(['bundle_sale_group']);
|
||||||
|
$table->dropForeign(['bundle_id']);
|
||||||
|
$table->dropColumn(['bundle_id', 'bundle_sale_group']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\App\BundleController;
|
||||||
use App\Http\Controllers\App\CashRegisterController;
|
use App\Http\Controllers\App\CashRegisterController;
|
||||||
use App\Http\Controllers\App\CategoryController;
|
use App\Http\Controllers\App\CategoryController;
|
||||||
use App\Http\Controllers\App\ClientController;
|
use App\Http\Controllers\App\ClientController;
|
||||||
@ -75,6 +76,16 @@
|
|||||||
//CATEGORIAS
|
//CATEGORIAS
|
||||||
Route::resource('categorias', CategoryController::class);
|
Route::resource('categorias', CategoryController::class);
|
||||||
|
|
||||||
|
//BUNDLES/KITS
|
||||||
|
Route::prefix('bundles')->group(function () {
|
||||||
|
Route::get('/', [BundleController::class, 'index']);
|
||||||
|
Route::get('/{bundle}', [BundleController::class, 'show']);
|
||||||
|
Route::post('/', [BundleController::class, 'store']);
|
||||||
|
Route::put('/{bundle}', [BundleController::class, 'update']);
|
||||||
|
Route::delete('/{bundle}', [BundleController::class, 'destroy']);
|
||||||
|
Route::get('/{bundle}/check-stock', [BundleController::class, 'checkStock']);
|
||||||
|
});
|
||||||
|
|
||||||
//PRECIOS
|
//PRECIOS
|
||||||
Route::resource('precios', PriceController::class);
|
Route::resource('precios', PriceController::class);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user