food-market/docs/negative-stock-alerts-progress.md
nns c69d40a366
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
docs(stock): прогресс+mobile-api — центр уведомлений, агрегация/эскалация, каналы
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 03:14:17 +05:00

12 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. Без спама: фоновый джоб NegativeStockNotificationJob (cron */5) агрегирует новые минусы в ОДНО уведомление на склад, дебаунс по NotifiedAt + интервал орг. (по умолч. 15 мин), тихие часы (Asia/Almaty, настройка). Получатели — админы, владелец, кладовщики (право InventoryEdit). Тумблеры/интервал/тихие часы/эскалация — в настройках организации (GET/PUT settings + UI).
  5. «Пересчитать эти товары»: POST /api/negative-stock/recount → черновик инвентаризации по складу ровно с минусовыми товарами (BookQty = текущий отрицательный остаток), открывается из баннера, центра уведомлений и блока «Просроченные минусы».
  6. [~] Каналы: веб — постоянный баннер + фильтр/пометка в остатках ✓; центр уведомлений (колокольчик, API-backed: /api/notifications список/счётчик/read/read-all, живой через SignalR NotificationCreated) ✓; отчёт «Отрицательные» ✓. Telegram — рассылка владельцу org ✓. Мобильное API — GET /api/notifications + /count + POST …/read + recount ✓; GET /api/negative-stock + /count ✓. APNs-заглушка — device_tokens (Phase40) + IPushSender/ StubPushSender ✓. Отложено: Web Push (VAPID + SW push-обработчик) — браузерный канал.
  7. [~] Тесты: unit тихих часов 3/3 ✓; integration 3/3 (минус→алерт→recount→авто-закрытие; фильтр negativeOnly; джоб → уведомление админу + дебаунс + read-all) ✓; full-chain 9/13 ✓. Скрины стенда ✓ — баннер, фильтр, центр уведомлений (neg-notif-center2-*.png), настройки (neg-settings-*.png). Отложено: автоматический e2e-браузер-тест колокольчика/«Пересчитать».
  8. Снята блокировка продажи в минус: розница, касса (POS-синк), опт, списание, перемещение, возврат поставщику, отмена приёмки. Деньги (минус счёта) — не трогали (другое правило).
  9. Отложено (десктопная Avalonia-касса): предупреждение в строке + жёлтый индикатор в шапке + офлайн по локальным остаткам. Требует предварительного задела: POS-каталог (PosProduct/ ProductSyncDto) сейчас не синхронизирует остатки — нет поля количества; офлайн-предупреждение невозможно без добавления остатков в sync-протокол. Онлайн-индикатор мог бы поллить /api/negative-stock/count. Отдельная поверхность (SDK-пин, headless-UI).
  10. Админка: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка + фильтр «Только минусы».
  11. Мобильное API/iOS: счётчик /api/notifications/count + список /api/notifications + /api/ negative-stock + recount. Баннер в самом приложении — Mac-Claude.
  12. Эскалация: джоб повторно уведомляет владельца/админов по минусам старше N часов (настройка, по умолч. 24; EscalatedAt), тип negative-stock-overdue; на дашборде — блок «Просроченные минусы» с «Пересчитать». Флаг overdue в API.
  13. ОТЛОЖЕНО (поправка владельца 09.10) — предварительная себестоимость НЕ делаем.

Что сделано (ядро + уведомления, оттестировано и выложено)

Пасс 1: разрешение минуса + детекция/авто-закрытие + API (список/счётчик/recount/фильтр) + веб-баннер/ пометки + APNs-задел. Пасс 2: персистентный центр уведомлений (notifications, Phase41) + REST + фоновый джоб агрегации/эскалации (интервал/тихие часы/получатели по ролям) + Telegram + настройки орг.

  • блок «Просроченные минусы» + unit 3/3 + integration 3/3. Выложено на стенд, скрины сняты.

Что отложено (нужен отдельный проход)

  • Web Push (VAPID) — браузерный push-канал (ключи + SW push/notificationclick + подписка).
  • Автоматический e2e-браузер-тест колокольчика/«Пересчитать» (ручные скрины сняты).
  • Avalonia-касса (п.8) — индикатор/предупреждение; сначала нужен задел: остатки в POS-sync (у PosProduct нет количества). Отдельная десктоп-поверхность.
  • Реальная отправка push (APNs/FCM) — нужен платный Apple Developer; заглушка в DI готова. DONE не ставим — остаются п.5 (Web Push), п.6 (e2e-браузер), п.8 (касса).

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

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

Журнал

  • 09.10 — старт: прогресс-док, сверка с МойСкладом, обследование кода.
  • 09.10 — ядро выложено на стенд (9ba1edb): разрешение минуса + детекция/авто-закрытие + API (список/счётчик/recount/фильтр) + веб-баннер/пометки + integration 2/2.
  • 09.10 — пасс 2 (29a951a, 09ee056): центр уведомлений (notifications/Phase41) + REST + джоб агрегации/эскалации + Telegram + настройки орг. + «Просроченные минусы» + unit 3/3 + integration 3/3. Стенд: /api/notifications 200, settings-поля применились, джоб (cron */5) создаёт агрегат «23 товара ушли в минус» админу — проверено скринами. Фикс: popover вправо.
  • 09.10 — задел push (3547e82): device_tokens (Phase40) + IPushSender/StubPushSender + /api/me/device-tokens, описано в docs/mobile-api.md. Выложено на стенд, Phase39+Phase40 применились (/api/negative-stock/count → 200). Скрины баннера + фильтра минусов сняты.