From c69d40a366d2c23d3bbd80ccb70bb63896f85fe9 Mon Sep 17 00:00:00 2001 From: nns Date: Fri, 9 Oct 2026 03:14:17 +0500 Subject: [PATCH] =?UTF-8?q?docs(stock):=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D1=81+mobile-api=20=E2=80=94=20=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=80=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9,=20=D0=B0=D0=B3=D1=80=D0=B5=D0=B3=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F/=D1=8D=D1=81=D0=BA=D0=B0=D0=BB=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F,=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- docs/mobile-api.md | 12 +++++ docs/negative-stock-alerts-progress.md | 69 +++++++++++++++----------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/docs/mobile-api.md b/docs/mobile-api.md index 014a07c..b469ab5 100644 --- a/docs/mobile-api.md +++ b/docs/mobile-api.md @@ -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=…` — отзыв. diff --git a/docs/negative-stock-alerts-progress.md b/docs/negative-stock-alerts-progress.md index c74d8f6..20232a1 100644 --- a/docs/negative-stock-alerts-progress.md +++ b/docs/negative-stock-alerts-progress.md @@ -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). Скрины баннера + фильтра минусов сняты.