food-market/docs/negative-stock-alerts-progress.md
nns 2d1cdd8519
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
docs(stock): прогресс negative-stock — что сделано/отложено
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:19:37 +05:00

8.6 KiB
Raw Blame History

Уведомления об отрицательных остатках → инвентаризация

Идея владельца 08.10.2026: «когда у товара количество становится отрицательным, система и мобильное приложение должны отправлять PUSH о том, что необходимо произвести инвентаризацию по таким товарам, и сразу предоставить список таких товаров».

Решение владельца 08.10: разрешать продажу в минус нужно, но с постоянным предупреждением везде (касса, админка, приложение) и требованием срочно провести инвентаризацию.

Поправка 09.10: пункт 12 (предварительная себестоимость) НЕ делаем — тема отложена; себестоимость продаж в минус оставляем как сейчас, расчёт не трогаем.

Сверка с МойСкладом (что есть у них)

  • Отчёт «Остатки» (report/stock) с параметром stockMode: значения all, positiveOnly, negativeOnly, underminimum, nonEmpty, empty. Т.е. отрицательные остатки — это только фильтр в отчёте (stockMode=negativeOnly). Отдельного «уведомления» нет.
  • Вебхуки МойСклада — на CRUD-события сущностей (создание/изменение/удаление документов и справочников). Порога «остаток ушёл в минус» среди событий нет — вебхук на изменение документа есть, но «стало отрицательно» как события нет.
  • Минус разрешён настройкой («разрешить отрицательные остатки»); push/требования пересчёта нет.

Вывод: у МойСклада — пассивный отчёт-фильтр. Мы делаем лучше: проактивные предупреждения (событие при уходе в минус), постоянные баннеры/индикаторы в кассе/админке/приложении, push (web/telegram, APNs-заглушка), подсказка причины, и действие «Пересчитать» (авто-черновик инвентаризации по минусовым товарам), с авто-закрытием при возврате остатка ≥ 0 и эскалацией. (Источник по МойСкладу: dev.moysklad.ru/doc/api/remap/1.2 report/stock stockMode; webhooks — на сущности.)

План / статус (легенда: [x] готово · [~] частично/заложено · [ ] отложено)

  1. Сверка с МойСкладом (см. выше): у них — только фильтр отчёта stockMode=negativeOnly, push/события нет.
  2. Событие при проведении документа → остаток < 0: NegativeStockAlert (товар, склад, остаток, документ-причина: тип/номер/автор/время), единая точка — StockService.ApplyMovementAsync. Остаток ≥ 0 → закрывается автоматически. Миграция Phase39, частичный уникальный индекс.
  3. Подсказка причины: API списка отдаёт hint «возможно, не проведена приёмка П-… от <поставщик>» (если по товару есть черновик приёмки).
  4. [~] Без спама: счётчик/баннер — реалтайм-запрос (poll 60с), не спамит. Поля NotifiedAt/EscalatedAt в сущности заложены под фоновую агрегацию/рассылку. Отложено: фоновая job (email/telegram одним сообщением на склад, тихие часы, получатели по ролям/привязке, тумблеры в настройках).
  5. «Пересчитать эти товары»: POST /api/negative-stock/recount → черновик инвентаризации по складу ровно с минусовыми товарами (BookQty = текущий отрицательный остаток), открывается из баннера.
  6. [~] Каналы: веб — постоянный баннер + фильтр/пометка в остатках ✓; отчёт «Отрицательные» (фильтр negativeOnly) ✓. Мобильное API — GET /api/negative-stock + /count готовы (iOS читает) ✓. Отложено: центр уведомлений (realtime-событие NegativeStock), Telegram-рассылка, Web Push (VAPID + SW), APNs-заглушка (device tokens + IPushSender).
  7. [~] Тесты: integration 2/2 (минус разрешён → алерт → recount → авто-закрытие; фильтр negativeOnly) ✓; full-chain шаг 9/13 переписаны (oversell/отмена приёмки → проведено + предупреждение) ✓. Отложено: unit на агрегацию/дебаунс (нет job), e2e в браузере на баннер. Деплой/скрины — ниже.
  8. Снята блокировка продажи в минус: розница, касса (POS-синк), опт, списание, перемещение, возврат поставщику, отмена приёмки. Деньги (минус счёта) — не трогали (другое правило).
  9. Отложено (десктопная Avalonia-касса): предупреждение в строке + жёлтый индикатор в шапке + офлайн по локальным остаткам. (Отдельная поверхность; iOS/касса — Mac-Claude.)
  10. Админка: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка + фильтр «Только минусы».
  11. [~] Мобильное API/iOS: /api/negative-stock/count + список готовы. Баннер в приложении — Mac-Claude.
  12. [~] Эскалация: в API есть флаг overdue (минус висит дольше N часов, по умолч. 24). Отложено: фоновое повторное уведомление владельцу/админу + блок «Просроченные минусы» на главной.
  13. ОТЛОЖЕНО (поправка владельца 09.10) — предварительная себестоимость НЕ делаем.

Что сделано в этой сессии (ядро, оттестировано и выложено)

Разрешение минуса + детекция/авто-закрытие предупреждений + API (список/счётчик/recount/фильтр) + веб-баннер и пометки в остатках + integration-тесты 2/2 + full-chain 9/13. Деплой на стенд.

Что отложено (заложен фундамент, нужен отдельный проход)

Фоновая рассылка (email/telegram, агрегация/тихие часы/получатели/настройки), эскалация-job и «просроченные минусы», центр уведомлений (realtime-событие), Web Push (VAPID), APNs-заглушка (device tokens + IPushSender), индикаторы в десктопной Avalonia-кассе (в т.ч. офлайн). DONE не ставим — не все пункты закрыты.

Что НЕ трогаем

Прод, shared-postgres, global.json, gateway nginx, sklad. Расчёт себестоимости (п.12 отложен).

Журнал

  • 09.10 — старт: прогресс-док, сверка с МойСкладом, обследование кода.