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.
This commit is contained in:
@@ -12,6 +12,7 @@ use App\Repositories\Shop\Customers;
|
|||||||
use App\Repositories\Shop\CustomerAddresses;
|
use App\Repositories\Shop\CustomerAddresses;
|
||||||
use App\Repositories\Shop\Deliveries;
|
use App\Repositories\Shop\Deliveries;
|
||||||
use App\Repositories\Shop\DeliveryTypes;
|
use App\Repositories\Shop\DeliveryTypes;
|
||||||
|
use App\Repositories\Shop\Offers;
|
||||||
use App\Repositories\Shop\OrderMails;
|
use App\Repositories\Shop\OrderMails;
|
||||||
use App\Repositories\Shop\Orders;
|
use App\Repositories\Shop\Orders;
|
||||||
use App\Repositories\Shop\Paybox;
|
use App\Repositories\Shop\Paybox;
|
||||||
@@ -94,6 +95,25 @@ class OrderController extends Controller
|
|||||||
$data['customer_id'] = Customers::getId();
|
$data['customer_id'] = Customers::getId();
|
||||||
$data['sale_channel_id'] = $data['sale_channel_id'] ?? SaleChannels::getDefaultID();
|
$data['sale_channel_id'] = $data['sale_channel_id'] ?? SaleChannels::getDefaultID();
|
||||||
$data['basket'] = Baskets::getBasketSummary($data['sale_channel_id'], $data['delivery_type_id'] ?? false);
|
$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);
|
$order = Orders::saveOrder($data);
|
||||||
|
|
||||||
if ($order) {
|
if ($order) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class Articles
|
|||||||
'id' => $offer->id,
|
'id' => $offer->id,
|
||||||
'name' => $offer->variation->name,
|
'name' => $offer->variation->name,
|
||||||
'prices' => $offer->tariff->price_lists->first()->price_list_values->toArray(),
|
'prices' => $offer->tariff->price_lists->first()->price_list_values->toArray(),
|
||||||
|
'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ class Baskets
|
|||||||
'variation' => $offer->variation->name,
|
'variation' => $offer->variation->name,
|
||||||
'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)),
|
'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)),
|
||||||
'latin' => $offer->article->product->specie->latin ?? false,
|
'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']);
|
$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 [
|
return [
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'name' => self::getArticleName($offer),
|
'name' => self::getArticleName($offer),
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
'name' => 'quantity',
|
'name' => 'quantity',
|
||||||
'class' => 'quantity',
|
'class' => 'quantity',
|
||||||
'id_name' => $model . '-quantity',
|
'id_name' => $model . '-quantity',
|
||||||
'value' => (int) $data[0]['prices'][0]['quantity'],
|
'value' => 1,
|
||||||
'min' => $data[0]['prices'][0]['quantity'],
|
'min' => 1,
|
||||||
|
'max' => $data[0]['stock'] ?? false,
|
||||||
'step' => 1,
|
'step' => 1,
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
@@ -44,10 +45,35 @@
|
|||||||
|
|
||||||
@push('js')
|
@push('js')
|
||||||
<script>
|
<script>
|
||||||
|
var {{ $model }}Stocks = {
|
||||||
|
@foreach ($data as $offer)
|
||||||
|
{{ $offer['id'] }}: {{ $offer['stock'] !== null ? $offer['stock'] : 'null' }},
|
||||||
|
@endforeach
|
||||||
|
};
|
||||||
|
|
||||||
|
function update{{ ucfirst($model) }}Max() {
|
||||||
|
var offerId = $('#{{ $model }}-offer_id').find('option:selected').val();
|
||||||
|
var stock = {{ $model }}Stocks[offerId];
|
||||||
|
var input = $('#{{ $model }}-quantity');
|
||||||
|
if (stock !== null && stock !== undefined) {
|
||||||
|
input.attr('max', stock);
|
||||||
|
if (parseInt(input.val()) > stock) {
|
||||||
|
input.val(stock);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.removeAttr('max');
|
||||||
|
}
|
||||||
|
if (parseInt(input.val()) < 1) {
|
||||||
|
input.val(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$('#{{ $model }}-quantity').change(function() {
|
$('#{{ $model }}-quantity').change(function() {
|
||||||
|
update{{ ucfirst($model) }}Max();
|
||||||
setPrice('{{ $model }}');
|
setPrice('{{ $model }}');
|
||||||
});
|
});
|
||||||
$('#{{ $model }}-offer_id').change(function() {
|
$('#{{ $model }}-offer_id').change(function() {
|
||||||
|
update{{ ucfirst($model) }}Max();
|
||||||
setPrice('{{ $model }}');
|
setPrice('{{ $model }}');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -53,7 +53,12 @@
|
|||||||
|
|
||||||
$('.basket-quantity').change(function() {
|
$('.basket-quantity').change(function() {
|
||||||
var offer_id = $(this).data('id');
|
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');
|
var $row = $(this).closest('.row');
|
||||||
updateBasket(offer_id, quantity, function() {
|
updateBasket(offer_id, quantity, function() {
|
||||||
calculatePrice($row);
|
calculatePrice($row);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
'value' => $item['quantity'],
|
'value' => $item['quantity'],
|
||||||
'class' => 'basket-quantity',
|
'class' => 'basket-quantity',
|
||||||
'data_id' => $item['id'],
|
'data_id' => $item['id'],
|
||||||
|
'min' => 1,
|
||||||
|
'max' => $item['stock'] ?? false,
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 text-right" style="font-size: 2em;" id="basket_total-{{ $item['id'] }}">
|
<div class="col-4 text-right" style="font-size: 2em;" id="basket_total-{{ $item['id'] }}">
|
||||||
|
|||||||
Reference in New Issue
Block a user