Skip to content

[Feature] Складской учёт: не давать превысить доступный остаток при изменении корзины #809

Description

@biz87

Проблема

При включённой 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 (новый)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphpPull requests that update php codepriority: mediumСредний приоритет

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions