Проблема
При включённой ms3_inventory_enabled корзина остаток не проверяет. Покупатель кладёт 50 шт. при остатке 10, заполняет адрес, выбирает доставку и оплату и только на кнопке «Оформить» получает «Недостаточно остатка для товара N» — без указания, сколько доступно.
Сейчас проверка есть только в OrderSubmitHandler (assertOrderAvailable() до выдачи номера и повторно перед статусом «Новый»). В CartMutationHandler / CartItemManager учёт не вызывается. Единственное ограничение — ms3_cart_max_count (1000), с остатком не связанное.
Решение
Проверка в ядре — отдельный сервис, вызываемый из CartMutationHandler. Не плагин в пакете: плагины обновляются с UPDATE_OBJECT (_build/config.inc.php → update.plugins = true), поэтому правки магазина в поставляемом плагине теряются при каждом обновлении MS3. Гибкость для бизнеса даётся через настройку, событие и подмену сервиса — всё это переживает обновление.
1. Сервис CartInventoryGuard (Services/Cart/), ключ ms3_cart_inventory_guard
Вызывается в CartMutationHandler::add() и ::change() после msOnBeforeAddToCart / msOnBeforeChangeInCart, то есть с окончательным count — плагины, меняющие количество через returnedValues, не могут обойти проверку порядком выполнения.
Правила:
- Работает только если
ms3_inventory_enabled = 1 и ms3_inventory_cart_check = 1.
- Доступное количество — через
InventoryServiceInterface::getAvailable(), не прямым чтением stock. Внешний склад, подключённый подменой ms3_inventory, работает в корзине без доработок.
- Суммирование по
product_id, а не по строке корзины: остаток один на товар, варианты с разными опциями — разные строки. Сравнивается newCount + сумма остальных строк с тем же product_id против available.
- Уменьшение количества разрешено всегда. Остаток мог упасть, пока товар лежал в корзине (в корзине 8, остаток стал 5) — покупатель должен иметь возможность уменьшать 8 → 7. Блокируется только рост количества сверх доступного.
- При превышении — отказ, строка корзины не меняется. Молчаливое урезание не делаем: покупатель легко пропустит, что получил меньше, чем просил. Урезание можно реализовать своим плагином через событие (п. 3).
- Нулевой остаток (в т.ч.
NULL, который учёт считает нулём) — товар в корзину не попадает. Последовательно с оформлением, где такой заказ всё равно не пройдёт. Работа «под заказ» при включённом учёте — через ms3_inventory_cart_check = 0 или своё правило в событии.
Покрытие путей:
| Операция |
Проверка |
cart/add, новая строка |
да |
cart/add, товар уже в корзине |
через change() — покрыто |
cart/change |
да |
cart/change-option со слиянием строк |
через change() — покрыто |
cart/change-option без слияния |
не нужна: количество по product_id не меняется |
cart/remove |
не нужна |
2. Настройка ms3_inventory_cart_check
combo-boolean, по умолчанию 1, область ms3_statuses (рядом с ms3_inventory_enabled). Без эффекта при выключенном учёте. Позволяет оставить учёт и резервы на оформлении, но не ограничивать корзину.
3. Событие msOnCartInventoryCheck
Параметры: msOrder (черновик), product_id, product_key, requested (новое количество строки), in_cart (сумма других строк с тем же product_id), available, allowed (решение ядра).
Через returnedValues плагин может:
allowed — переопределить решение (разрешить превышение для категории / товара, запретить по своим правилам);
count — скорректировать количество (например, урезать до доступного) — скорректированное значение проходит тот же путь сохранения, что и обычное.
Событие регистрируется в _build/elements/events.php. Не вызывается, если проверка выключена настройками.
4. Ответ API при отказе
{
"success": false,
"message": "Доступно 5 шт. товара «X», в корзине уже 3",
"data": { "product_id": 12, "product_key": "...", "available": 5, "in_cart": 3 }
}
available позволяет витрине выставить максимум в поле количества. Новые ключи лексикона ru/en (ms3_cart_err_inventory, вариант с in_cart для случая, когда товар уже в корзине).
5. Ошибка при оформлении
В том же PR: ms3_err_inventory_insufficient при оформлении показывает название товара и доступное количество, а не только id.
Что это не делает
Проверка корзины — не резерв. Два покупателя могут одновременно положить по 10 шт. при остатке 10; выкупит тот, кто первым оформит. Главной остаётся проверка при оформлении. Резерв на этапе корзины сознательно не вводим — брошенные корзины держали бы остаток, понадобились бы TTL и крон-очистка.
Тесты
Unit-тесты CartInventoryGuard на InMemoryInventoryStockStore:
- выключен учёт / выключена
ms3_inventory_cart_check — проверка пропускается;
- добавление в пределах и сверх остатка;
- два варианта одного товара (разные опции) суммируются;
- уменьшение при
count > available разрешено, увеличение — нет;
- нулевой и
NULL остаток;
- переопределение решения и
count через событие.
Вне рамок (отдельные задачи при необходимости)
Файлы
core/components/minishop3/src/Services/Cart/CartInventoryGuard.php (новый)
core/components/minishop3/src/Services/Cart/CartMutationHandler.php
core/components/minishop3/src/Controllers/Cart/Cart.php (сборка зависимостей)
core/components/minishop3/src/ServiceRegistry.php / ServiceRegistryFactories.php
_build/elements/settings.php, _build/elements/events.php
core/components/minishop3/lexicon/{ru,en}/default.inc.php, setting.inc.php
core/components/minishop3/tests/Unit/Services/Cart/CartInventoryGuardTest.php (новый)
Проблема
При включённой
ms3_inventory_enabledкорзина остаток не проверяет. Покупатель кладёт 50 шт. при остатке 10, заполняет адрес, выбирает доставку и оплату и только на кнопке «Оформить» получает «Недостаточно остатка для товара N» — без указания, сколько доступно.Сейчас проверка есть только в
OrderSubmitHandler(assertOrderAvailable()до выдачи номера и повторно перед статусом «Новый»). ВCartMutationHandler/CartItemManagerучёт не вызывается. Единственное ограничение —ms3_cart_max_count(1000), с остатком не связанное.Решение
Проверка в ядре — отдельный сервис, вызываемый из
CartMutationHandler. Не плагин в пакете: плагины обновляются сUPDATE_OBJECT(_build/config.inc.php→update.plugins = true), поэтому правки магазина в поставляемом плагине теряются при каждом обновлении MS3. Гибкость для бизнеса даётся через настройку, событие и подмену сервиса — всё это переживает обновление.1. Сервис
CartInventoryGuard(Services/Cart/), ключms3_cart_inventory_guardВызывается в
CartMutationHandler::add()и::change()послеmsOnBeforeAddToCart/msOnBeforeChangeInCart, то есть с окончательнымcount— плагины, меняющие количество черезreturnedValues, не могут обойти проверку порядком выполнения.Правила:
ms3_inventory_enabled = 1иms3_inventory_cart_check = 1.InventoryServiceInterface::getAvailable(), не прямым чтениемstock. Внешний склад, подключённый подменойms3_inventory, работает в корзине без доработок.product_id, а не по строке корзины: остаток один на товар, варианты с разными опциями — разные строки. СравниваетсяnewCount + сумма остальных строк с тем же product_idпротивavailable.NULL, который учёт считает нулём) — товар в корзину не попадает. Последовательно с оформлением, где такой заказ всё равно не пройдёт. Работа «под заказ» при включённом учёте — черезms3_inventory_cart_check = 0или своё правило в событии.Покрытие путей:
cart/add, новая строкаcart/add, товар уже в корзинеchange()— покрытоcart/changecart/change-optionсо слиянием строкchange()— покрытоcart/change-optionбез слиянияproduct_idне меняетсяcart/remove2. Настройка
ms3_inventory_cart_checkcombo-boolean, по умолчанию1, областьms3_statuses(рядом сms3_inventory_enabled). Без эффекта при выключенном учёте. Позволяет оставить учёт и резервы на оформлении, но не ограничивать корзину.3. Событие
msOnCartInventoryCheckПараметры:
msOrder(черновик),product_id,product_key,requested(новое количество строки),in_cart(сумма других строк с тем жеproduct_id),available,allowed(решение ядра).Через
returnedValuesплагин может:allowed— переопределить решение (разрешить превышение для категории / товара, запретить по своим правилам);count— скорректировать количество (например, урезать до доступного) — скорректированное значение проходит тот же путь сохранения, что и обычное.Событие регистрируется в
_build/elements/events.php. Не вызывается, если проверка выключена настройками.4. Ответ API при отказе
{ "success": false, "message": "Доступно 5 шт. товара «X», в корзине уже 3", "data": { "product_id": 12, "product_key": "...", "available": 5, "in_cart": 3 } }availableпозволяет витрине выставить максимум в поле количества. Новые ключи лексикона ru/en (ms3_cart_err_inventory, вариант сin_cartдля случая, когда товар уже в корзине).5. Ошибка при оформлении
В том же PR:
ms3_err_inventory_insufficientпри оформлении показывает название товара и доступное количество, а не толькоid.Что это не делает
Проверка корзины — не резерв. Два покупателя могут одновременно положить по 10 шт. при остатке 10; выкупит тот, кто первым оформит. Главной остаётся проверка при оформлении. Резерв на этапе корзины сознательно не вводим — брошенные корзины держали бы остаток, понадобились бы TTL и крон-очистка.
Тесты
Unit-тесты
CartInventoryGuardнаInMemoryInventoryStockStore:ms3_inventory_cart_check— проверка пропускается;count > availableразрешено, увеличение — нет;NULLостаток;countчерез событие.Вне рамок (отдельные задачи при необходимости)
cart/get, чтобы витрина подсвечивала позиции, где остаток упал после добавления.maxу поля количества).Файлы
core/components/minishop3/src/Services/Cart/CartInventoryGuard.php(новый)core/components/minishop3/src/Services/Cart/CartMutationHandler.phpcore/components/minishop3/src/Controllers/Cart/Cart.php(сборка зависимостей)core/components/minishop3/src/ServiceRegistry.php/ServiceRegistryFactories.php_build/elements/settings.php,_build/elements/events.phpcore/components/minishop3/lexicon/{ru,en}/default.inc.php,setting.inc.phpcore/components/minishop3/tests/Unit/Services/Cart/CartInventoryGuardTest.php(новый)