Пасс 3: касса (индикатор+предупреждение+офлайн), Web Push (VAPID), e2e колокольчика. Итог: unit 299/299, integration 4/4, POS 4/4, e2e-браузер 2/2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
Уведомления об отрицательных остатках → инвентаризация
Идея владельца 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] готово · [~] частично/заложено · [ ] отложено)
- Сверка с МойСкладом (см. выше): у них — только фильтр отчёта
stockMode=negativeOnly, push/события нет. - Событие при проведении документа → остаток < 0:
NegativeStockAlert(товар, склад, остаток, документ-причина: тип/номер/автор/время), единая точка —StockService.ApplyMovementAsync. Остаток ≥ 0 → закрывается автоматически. Миграция Phase39, частичный уникальный индекс. - Подсказка причины: API списка отдаёт
hint«возможно, не проведена приёмка П-… от <поставщик>» (если по товару есть черновик приёмки). - Без спама: фоновый джоб
NegativeStockNotificationJob(cron */5) агрегирует новые минусы в ОДНО уведомление на склад, дебаунс поNotifiedAt+ интервал орг. (по умолч. 15 мин), тихие часы (Asia/Almaty, настройка). Получатели — админы, владелец, кладовщики (право InventoryEdit). Тумблеры/интервал/тихие часы/эскалация — в настройках организации (GET/PUT settings + UI). - «Пересчитать эти товары»:
POST /api/negative-stock/recount→ черновик инвентаризации по складу ровно с минусовыми товарами (BookQty = текущий отрицательный остаток), открывается из баннера, центра уведомлений и блока «Просроченные минусы». - Каналы: веб — постоянный баннер + фильтр/пометка в остатках ✓; центр уведомлений
(колокольчик, API-backed:
/api/notificationsсписок/счётчик/read/read-all, живой через SignalRNotificationCreated) ✓; отчёт «Отрицательные» ✓. Web Push (VAPID) — ключи генерятся и хранятся в platform_settings (Phase42),/api/web-push/{public-key,subscribe,unsubscribe}, SW-обработчики push/notificationclick (am-v4), кнопка «Вкл. push» в колокольчике; работает в т.ч. iOS Safari PWA ✓. Telegram — рассылка владельцу org ✓. Мобильное API —/api/notifications(+count/read) + recount ✓;GET /api/negative-stock+/count✓. APNs-заглушка —device_tokens(Phase40) +IPushSender/StubPushSender✓. Прим.: фактическую доставку Web Push в браузер нельзя проверить headless (нужен живой push-сервис) — покрыт путь генерации ключа/подписки/отправки. - Тесты: unit 3/3 тихих часов (вся юнит-сьют 299/299, попутно починен SQLite-харнесс + тесты POS-остатков); integration 4/4 (минус→алерт→recount→авто-закрытие; фильтр negativeOnly; джоб → уведомление админу + дебаунс + read-all; Web Push ключ/подписка); POS 4/4 (снимок остатков/перезапись/NegativeCount); e2e-браузер 2/2 (баннер+«Пересчитать»+фильтр; колокольчик с ожиданием cron-джоба) ✓; full-chain 9/13 ✓. Деплой на стенд + скрины: баннер, фильтр, центр уведомлений, настройки, Push-тумблер, касса (индикатор + предупреждение в строке).
- Снята блокировка продажи в минус: розница, касса (POS-синк), опт, списание, перемещение, возврат поставщику, отмена приёмки. Деньги (минус счёта) — не трогали (другое правило).
- Касса (Avalonia): при количестве больше остатка — предупреждение прямо в строке
«на остатке N шт — уйдёт в минус» (жёлтое, НЕ блокирует продажу); в шапке — постоянный
жёлтый индикатор «N в минусе — нужна инвентаризация» по складу кассы, по тапу — список (Flyout).
Работает офлайн по локальному снимку остатков: сервер уже слал остатки в sync (
StockSyncDto), касса теперь их хранит (таблицаstock, перезапись каждый синк). Обновляется при старте/после чека/после синка. Тесты PosStore 4/4, скрин кассы снят. - Админка: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка + фильтр «Только минусы».
- Мобильное API/iOS: счётчик
/api/notifications/count+ список/api/notifications+/api/ negative-stock+ recount. Баннер в самом приложении — Mac-Claude. - Эскалация: джоб повторно уведомляет владельца/админов по минусам старше N часов (настройка, по
умолч. 24;
EscalatedAt), типnegative-stock-overdue; на дашборде — блок «Просроченные минусы» с «Пересчитать». Флагoverdueв API. - ОТЛОЖЕНО (поправка владельца 09.10) — предварительная себестоимость НЕ делаем.
Что сделано (всё в рамках задачи, оттестировано и выложено)
Пасс 1: разрешение минуса + детекция/авто-закрытие + API (список/счётчик/recount/фильтр) + веб-баннер/
пометки + APNs-задел. Пасс 2: персистентный центр уведомлений (notifications, Phase41) + REST +
фоновый джоб агрегации/эскалации (интервал/тихие часы/получатели по ролям) + Telegram + настройки орг.
- блок «Просроченные минусы». Пасс 3: касса (индикатор + предупреждение в строке + офлайн, Avalonia) + Web Push (VAPID) (Phase42, SW push/notificationclick, кнопка «Вкл. push») + e2e-браузер колокольчика. Итог: unit 299/299, integration 4/4, POS 4/4, e2e-браузер 2/2. Выложено на стенд, скрины сняты.
Остаётся за рамками этой задачи
- Пункт 12 (предварительная себестоимость) — ОТЛОЖЕН владельцем (поправка 09.10), не делаем.
- Реальная отправка APNs/FCM push на iOS — нужен платный Apple Developer; задел (device_tokens + IPushSender + StubPushSender) готов, подменяется реальной реализацией без правок вызова.
- Прим.: фактическую доставку Web Push в реальный браузер нельзя проверить в headless-CI (нужен живой push-сервис) — реализация полная (web-push-csharp, RFC 8291), проверен путь ключ/подписка/отправка.
Все пункты 1–11 выполнены (п.12 отложен владельцем) → DONE.
Что НЕ трогаем
Прод, 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/notifications200, 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). Скрины баннера + фильтра минусов сняты. - 09.10 — пасс 3: касса (
0fd04d4) — остатки в PosStore (таблица stock), предупреждение в строке + жёлтый индикатор в шапке (офлайн), PosStore-тесты 4/4, скрин кассы. Web Push VAPID (560d52e) — platform_settings-ключи (Phase42),/api/web-push/*, SW am-v4, кнопка «Вкл. push»; стенд: public-key 87 симв., subscribe/unsubscribe 204. e2e колокольчика (ff42db7) — ждёт cron-джоб, 2/2 зелёные. Все пункты 1–11 закрыты (п.12 отложен владельцем).