docs(stock): прогресс+mobile-api — центр уведомлений, агрегация/эскалация, каналы
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 03:14:17 +05:00
parent 09ee0562c2
commit c69d40a366
2 changed files with 53 additions and 28 deletions

View file

@ -126,6 +126,18 @@ deprecated после обновления iOS (iOS обновит Mac-Claude).
`{ inventoryId, number, lines }`. Открыть через обычный экран инвентаризации.
- Отчёт остатков: `GET /api/inventory/stock?negativeOnly=true` — только минусы.
### Центр уведомлений (персистентный)
Фоновый джоб кладёт агрегат «N товаров ушли в минус» на склад (дебаунс/тихие часы) и эскалацию
просроченных минусов в таблицу `notifications`. iOS читает тот же фид, что и веб-колокольчик.
- `GET /api/notifications[?unreadOnly=&take=50]` → `{ items:[{ id, type, severity, title, body,
link, dataJson, storeId, read, createdAt }], unread, total }`. `type` = `negative-stock` |
`negative-stock-overdue`; `dataJson` содержит `{ storeId, count, productIds[] }` для действия
«Пересчитать».
- `GET /api/notifications/count` → `{ unread }` — для бейджа.
- `POST /api/notifications/{id}/read` (204) — пометить одно прочитанным; `POST /api/notifications/read-all`
(204) — все.
- Создать инвентаризацию по предупреждениям — `POST /api/negative-stock/recount` `{ storeId }` (см. выше).
### Push-токены устройств (заложено; реальный APNs позже)
- `POST /api/me/device-tokens` body `{ platform: "ios"|"android"|"web", token }` — регистрация
токена (идемпотентно по токену). `DELETE /api/me/device-tokens?token=…` — отзыв.

View file

@ -32,43 +32,52 @@
Остаток ≥ 0 → закрывается автоматически. Миграция Phase39, частичный уникальный индекс.
2. [x] Подсказка причины: API списка отдаёт `hint` «возможно, не проведена приёмка П-… от <поставщик>»
(если по товару есть черновик приёмки).
3. [~] Без спама: счётчик/баннер — реалтайм-запрос (poll 60с), не спамит. Поля `NotifiedAt`/`EscalatedAt`
в сущности заложены под фоновую агрегацию/рассылку. **Отложено:** фоновая job (email/telegram
одним сообщением на склад, тихие часы, получатели по ролям/привязке, тумблеры в настройках).
3. [x] Без спама: фоновый джоб `NegativeStockNotificationJob` (cron */5) агрегирует новые минусы в
ОДНО уведомление на склад, дебаунс по `NotifiedAt` + интервал орг. (по умолч. 15 мин), тихие
часы (Asia/Almaty, настройка). Получатели — админы, владелец, кладовщики (право InventoryEdit).
Тумблеры/интервал/тихие часы/эскалация — в настройках организации (GET/PUT settings + UI).
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 в браузере на баннер.
ровно с минусовыми товарами (BookQty = текущий отрицательный остаток), открывается из баннера,
центра уведомлений и блока «Просроченные минусы».
5. [~] Каналы: **веб** — постоянный баннер + фильтр/пометка в остатках ✓; **центр уведомлений**
(колокольчик, 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-обработчик) — браузерный канал.
6. [~] Тесты: unit тихих часов **3/3** ✓; integration **3/3** (минус→алерт→recount→авто-закрытие;
фильтр negativeOnly; **джоб → уведомление админу + дебаунс + read-all**) ✓; full-chain 9/13 ✓.
Скрины стенда ✓ — баннер, фильтр, **центр уведомлений** (`neg-notif-center2-*.png`), **настройки**
(`neg-settings-*.png`). **Отложено:** автоматический e2e-браузер-тест колокольчика/«Пересчитать».
7. [x] Снята блокировка продажи в минус: розница, касса (POS-синк), опт, списание, перемещение,
возврат поставщику, отмена приёмки. Деньги (минус счёта) — не трогали (другое правило).
8. [ ] **Отложено** (десктопная Avalonia-касса): предупреждение в строке + жёлтый индикатор в шапке +
офлайн по локальным остаткам. (Отдельная поверхность; iOS/касса — Mac-Claude.)
офлайн по локальным остаткам. Требует предварительного задела: POS-каталог (`PosProduct`/
`ProductSyncDto`) сейчас **не синхронизирует остатки** — нет поля количества; офлайн-предупреждение
невозможно без добавления остатков в sync-протокол. Онлайн-индикатор мог бы поллить
`/api/negative-stock/count`. Отдельная поверхность (SDK-пин, headless-UI).
9. [x] Админка: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком
не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка + фильтр «Только минусы».
10. [~] Мобильное API/iOS: `/api/negative-stock/count` + список готовы. Баннер в приложении — Mac-Claude.
11. [~] Эскалация: в API есть флаг `overdue` (минус висит дольше N часов, по умолч. 24). **Отложено:**
фоновое повторное уведомление владельцу/админу + блок «Просроченные минусы» на главной.
10. [x] Мобильное API/iOS: счётчик `/api/notifications/count` + список `/api/notifications` + `/api/
negative-stock` + recount. Баннер в самом приложении — Mac-Claude.
11. [x] Эскалация: джоб повторно уведомляет владельца/админов по минусам старше N часов (настройка, по
умолч. 24; `EscalatedAt`), тип `negative-stock-overdue`; на дашборде — блок «Просроченные минусы»
с «Пересчитать». Флаг `overdue` в API.
12. [ ] ОТЛОЖЕНО (поправка владельца 09.10) — предварительная себестоимость НЕ делаем.
## Что сделано в этой сессии (ядро, оттестировано и выложено)
Разрешение минуса + детекция/авто-закрытие предупреждений + API (список/счётчик/recount/фильтр) +
веб-баннер и пометки в остатках + integration-тесты 2/2 + full-chain 9/13. Деплой на стенд.
## Что сделано (ядро + уведомления, оттестировано и выложено)
Пасс 1: разрешение минуса + детекция/авто-закрытие + API (список/счётчик/recount/фильтр) + веб-баннер/
пометки + APNs-задел. Пасс 2: персистентный центр уведомлений (`notifications`, Phase41) + REST +
фоновый джоб агрегации/эскалации (интервал/тихие часы/получатели по ролям) + Telegram + настройки орг.
+ блок «Просроченные минусы» + unit 3/3 + integration 3/3. Выложено на стенд, скрины сняты.
## Что отложено (заложен фундамент, нужен отдельный проход)
Фоновая рассылка (email/telegram, агрегация/тихие часы/получатели/настройки), эскалация-job и
«просроченные минусы», центр уведомлений (realtime-событие), Web Push (VAPID), **реальная**
отправка push (APNs/FCM — заглушка уже в DI), индикаторы в десктопной Avalonia-кассе (в т.ч. офлайн).
**DONE не ставим — не все пункты закрыты.**
## Что отложено (нужен отдельный проход)
- **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 отложен).
@ -77,6 +86,10 @@
- 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). Скрины баннера + фильтра минусов сняты.