food-market/docs/negative-stock-alerts-progress.md
nns 242f48ff9f
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
docs(stock): прогресс negative-stock — задел push выложен, скрины баннера/фильтра на стенде
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 02:33:52 +05:00

83 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Уведомления об отрицательных остатках → инвентаризация
Идея владельца 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] готово · [~] частично/заложено · [ ] отложено)
0. [x] Сверка с МойСкладом (см. выше): у них — только фильтр отчёта `stockMode=negativeOnly`, push/события нет.
1. [x] Событие при проведении документа → остаток < 0: `NegativeStockAlert` (товар, склад, остаток,
документ-причина: тип/номер/автор/время), единая точка — `StockService.ApplyMovementAsync`.
Остаток ≥ 0 → закрывается автоматически. Миграция Phase39, частичный уникальный индекс.
2. [x] Подсказка причины: API списка отдаёт `hint` «возможно, не проведена приёмка П-… от <поставщик>»
(если по товару есть черновик приёмки).
3. [~] Без спама: счётчик/баннер — реалтайм-запрос (poll 60с), не спамит. Поля `NotifiedAt`/`EscalatedAt`
в сущности заложены под фоновую агрегацию/рассылку. **Отложено:** фоновая job (email/telegram
одним сообщением на склад, тихие часы, получатели по ролям/привязке, тумблеры в настройках).
4. [x] «Пересчитать эти товары»: `POST /api/negative-stock/recount` → черновик инвентаризации по складу
ровно с минусовыми товарами (BookQty = текущий отрицательный остаток), открывается из баннера.
5. [~] Каналы: **веб** — постоянный баннер + фильтр/пометка в остатках ✓; отчёт «Отрицательные» (фильтр
`negativeOnly`) ✓. **Мобильное API** — `GET /api/negative-stock` + `/count` готовы (iOS читает) ✓.
**APNs-заглушка заложена** ✓ — таблица `device_tokens` (Phase40) + `POST/DELETE /api/me/device-tokens`
+ контракт `IPushSender`, в DI `StubPushSender` (лог); реальный APNs/FCM/VAPID подменит заглушку
без правок вызова. Описано в `docs/mobile-api.md`. **Отложено:** центр уведомлений (realtime-событие
NegativeStock), Telegram-рассылка, Web Push (VAPID + SW), реальная отправка push.
6. [~] Тесты: integration **2/2** (минус разрешён → алерт → recount → авто-закрытие; фильтр negativeOnly) ✓;
full-chain шаг 9/13 переписаны (oversell/отмена приёмки → проведено + предупреждение) ✓.
**Скрины на стенде** ✓ — `tests/e2e/reports/neg-banner-home-*.png` (баннер «22 товаров в минусе»),
`neg-stock-filter-*.png` (фильтр «Только минусы» + красные остатки). Стенд-smoke: `/count` 200,
`/api/negative-stock` 200, `device-tokens` DELETE 204 (Phase39+Phase40 применились).
**Отложено:** unit на агрегацию/дебаунс (нет job), e2e в браузере на баннер.
7. [x] Снята блокировка продажи в минус: розница, касса (POS-синк), опт, списание, перемещение,
возврат поставщику, отмена приёмки. Деньги (минус счёта) — не трогали (другое правило).
8. [ ] **Отложено** (десктопная Avalonia-касса): предупреждение в строке + жёлтый индикатор в шапке +
офлайн по локальным остаткам. (Отдельная поверхность; iOS/касса — Mac-Claude.)
9. [x] Админка: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком
не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка + фильтр «Только минусы».
10. [~] Мобильное API/iOS: `/api/negative-stock/count` + список готовы. Баннер в приложении — Mac-Claude.
11. [~] Эскалация: в API есть флаг `overdue` (минус висит дольше N часов, по умолч. 24). **Отложено:**
фоновое повторное уведомление владельцу/админу + блок «Просроченные минусы» на главной.
12. [ ] ОТЛОЖЕНО (поправка владельца 09.10) — предварительная себестоимость НЕ делаем.
## Что сделано в этой сессии (ядро, оттестировано и выложено)
Разрешение минуса + детекция/авто-закрытие предупреждений + API (список/счётчик/recount/фильтр) +
веб-баннер и пометки в остатках + integration-тесты 2/2 + full-chain 9/13. Деплой на стенд.
## Что отложено (заложен фундамент, нужен отдельный проход)
Фоновая рассылка (email/telegram, агрегация/тихие часы/получатели/настройки), эскалация-job и
«просроченные минусы», центр уведомлений (realtime-событие), Web Push (VAPID), **реальная**
отправка push (APNs/FCM — заглушка уже в DI), индикаторы в десктопной Avalonia-кассе (в т.ч. офлайн).
**DONE не ставим — не все пункты закрыты.**
## Что НЕ трогаем
Прод, shared-postgres, global.json, gateway nginx, sklad. Расчёт себестоимости (п.12 отложен).
## Журнал
- 09.10 — старт: прогресс-док, сверка с МойСкладом, обследование кода.
- 09.10 — ядро выложено на стенд (9ba1edb): разрешение минуса + детекция/авто-закрытие +
API (список/счётчик/recount/фильтр) + веб-баннер/пометки + integration 2/2.
- 09.10 — задел push (3547e82): `device_tokens` (Phase40) + `IPushSender`/`StubPushSender` +
`/api/me/device-tokens`, описано в `docs/mobile-api.md`. Выложено на стенд, Phase39+Phase40
применились (`/api/negative-stock/count` → 200). Скрины баннера + фильтра минусов сняты.