From 63673117b3a20eb48a865f45344e30d6d9ca5e67 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Fri, 20 Feb 2026 12:17:22 +0100 Subject: [PATCH] fix: enforce stock limits on basket quantities No stock validation existed in the ordering flow, allowing customers to order more items than available. Cap quantity to ``stock_current`` in ``Baskets::getBasketData()`` when adding to cart. Add ``min=1`` and ``max=stock`` attributes on the basket quantity input, with JS clamping in the change handler. Verify stock again in ``Shop\OrderController::store()`` before saving the order as a race-condition safeguard. --- app/Http/Controllers/Shop/OrderController.php | 20 +++++++++++++ app/Repositories/Shop/Articles.php | 1 + app/Repositories/Shop/Baskets.php | 8 +++++ .../Articles/partials/addBasket.blade.php | 30 +++++++++++++++++-- resources/views/Shop/Baskets/basket.blade.php | 7 ++++- .../Shop/Baskets/partials/article.blade.php | 2 ++ 6 files changed, 65 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Shop/OrderController.php b/app/Http/Controllers/Shop/OrderController.php index 95db3c31..a1b72c4d 100644 --- a/app/Http/Controllers/Shop/OrderController.php +++ b/app/Http/Controllers/Shop/OrderController.php @@ -12,6 +12,7 @@ use App\Repositories\Shop\Customers; use App\Repositories\Shop\CustomerAddresses; use App\Repositories\Shop\Deliveries; use App\Repositories\Shop\DeliveryTypes; +use App\Repositories\Shop\Offers; use App\Repositories\Shop\OrderMails; use App\Repositories\Shop\Orders; use App\Repositories\Shop\Paybox; @@ -94,6 +95,25 @@ class OrderController extends Controller $data['customer_id'] = Customers::getId(); $data['sale_channel_id'] = $data['sale_channel_id'] ?? SaleChannels::getDefaultID(); $data['basket'] = Baskets::getBasketSummary($data['sale_channel_id'], $data['delivery_type_id'] ?? false); + + // Vérifier le stock avant de valider la commande + $insufficients = []; + foreach ($data['basket']['detail'] as $item) { + $offer = Offers::get($item['offer_id']); + if ($offer && $offer->stock_current !== null && $item['quantity'] > $offer->stock_current) { + $offer->load('article'); + $insufficients[] = ($offer->article->name ?? 'Offre #'.$offer->id) + .' (stock : '.(int) $offer->stock_current.', demandé : '.$item['quantity'].')'; + } + } + + if (! empty($insufficients)) { + return redirect()->back()->withInput()->with( + 'growl', + ['Stock insuffisant pour : '.implode(' ; ', $insufficients).'. Veuillez ajuster les quantités.', 'danger'] + ); + } + $order = Orders::saveOrder($data); if ($order) { diff --git a/app/Repositories/Shop/Articles.php b/app/Repositories/Shop/Articles.php index 5c4a0683..f16e3bbe 100644 --- a/app/Repositories/Shop/Articles.php +++ b/app/Repositories/Shop/Articles.php @@ -43,6 +43,7 @@ class Articles 'id' => $offer->id, 'name' => $offer->variation->name, 'prices' => $offer->tariff->price_lists->first()->price_list_values->toArray(), + 'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null, ]; } diff --git a/app/Repositories/Shop/Baskets.php b/app/Repositories/Shop/Baskets.php index 53b04957..a3ad51ae 100644 --- a/app/Repositories/Shop/Baskets.php +++ b/app/Repositories/Shop/Baskets.php @@ -116,6 +116,7 @@ class Baskets 'variation' => $offer->variation->name, 'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)), 'latin' => $offer->article->product->specie->latin ?? false, + 'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null, ]; } @@ -144,6 +145,13 @@ class Baskets { $offer = Offers::get($id, ['article', 'variation']); + if ($offer && $offer->stock_current !== null) { + $quantity = min($quantity, max(0, (int) $offer->stock_current)); + if ($quantity <= 0) { + return false; + } + } + return [ 'id' => $id, 'name' => self::getArticleName($offer), diff --git a/resources/views/Shop/Articles/partials/addBasket.blade.php b/resources/views/Shop/Articles/partials/addBasket.blade.php index bae6ef7e..eaf3071b 100644 --- a/resources/views/Shop/Articles/partials/addBasket.blade.php +++ b/resources/views/Shop/Articles/partials/addBasket.blade.php @@ -18,8 +18,9 @@ 'name' => 'quantity', 'class' => 'quantity', 'id_name' => $model . '-quantity', - 'value' => (int) $data[0]['prices'][0]['quantity'], - 'min' => $data[0]['prices'][0]['quantity'], + 'value' => 1, + 'min' => 1, + 'max' => $data[0]['stock'] ?? false, 'step' => 1, ]) @@ -44,10 +45,35 @@ @push('js') diff --git a/resources/views/Shop/Baskets/basket.blade.php b/resources/views/Shop/Baskets/basket.blade.php index 2f67f1a0..e0b59c43 100644 --- a/resources/views/Shop/Baskets/basket.blade.php +++ b/resources/views/Shop/Baskets/basket.blade.php @@ -53,7 +53,12 @@ $('.basket-quantity').change(function() { var offer_id = $(this).data('id'); - var quantity = $(this).val(); + var quantity = parseInt($(this).val()) || 1; + var min = parseInt($(this).attr('min')) || 1; + var max = parseInt($(this).attr('max')); + if (quantity < min) quantity = min; + if (max && quantity > max) quantity = max; + $(this).val(quantity); var $row = $(this).closest('.row'); updateBasket(offer_id, quantity, function() { calculatePrice($row); diff --git a/resources/views/Shop/Baskets/partials/article.blade.php b/resources/views/Shop/Baskets/partials/article.blade.php index 57494e17..d3f02033 100644 --- a/resources/views/Shop/Baskets/partials/article.blade.php +++ b/resources/views/Shop/Baskets/partials/article.blade.php @@ -18,6 +18,8 @@ 'value' => $item['quantity'], 'class' => 'basket-quantity', 'data_id' => $item['id'], + 'min' => 1, + 'max' => $item['stock'] ?? false, ])