docs(mobile-api): описание API для мобильного клиента + прогресс
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-08 05:39:37 +05:00
parent 14001069a2
commit ad812ee2b5
2 changed files with 122 additions and 0 deletions

View file

@ -0,0 +1,45 @@
# Доработки сервера для iOS «FM Склад» — прогресс
Автономная задача владельца (08.10.2026). Приложение (SwiftUI) на Маке владельца
использует API стенда; серверу не хватает ряда возможностей. Стенд — не трогаем прод.
## План (пункты владельца)
1. [x] `/api/inventory/stock?groupId=` — фильтр по группе с вложенными подгруппами
(через денормализованный `ProductGroup.Path`, helper `DescendantGroupIdsAsync`).
2. [x] POST `/api/catalog/products/{id}/barcodes` {code, kind} — добавление штрихкода,
уникальность в орг (`(OrganizationId,Code)`), 409 «штрихкод уже у товара X» + название,
идемпотентно при повторе тому же товару, 400 на пустой, автоопределение типа по длине.
3. [x] `/api/me` → `permissions` (эффективные флаги `RolePermissions` с учётом роли и
простого/расширенного режима; единый источник `RolePermissions.Effective`). Веб —
хук `useCan()` на том же источнике (раньше гейтил по именам ролей/advancedMode).
4. [x] Офлайн-каталог (`ProductSyncDto`): добавлен явный `isWeighed` (+ уже были `packaging`,
`unitCode`).
5. [x] Весовые этикетки: настройки организации (`WeightBarcode*`: enabled/префиксы веса/
префиксы цены/длина кода), миграция Phase35; парсер вынесен в
`food-market.shared/Barcodes/WeightedBarcode` (кассы pos.core делегирует туда);
`by-barcode` разбирает весовую этикетку → товар + количество (вес) / встроенная цена.
`GET/PUT /api/organization/settings` отдаёт/принимает настройки (round-trip-safe).
6. [x] Тесты: unit 10 (WeightedBarcode + RolePermissions.Effective) + 4 integration
(группа/штрихкод/me-права/весовая этикетка); `docs/mobile-api.md`. Деплой на стенд — см. ниже.
НЕ трогать: прод, shared-postgres, global.json, gateway nginx, sklad. — соблюдено.
## Изученная база
- `/api/inventory/stock` — `StockController.GetStock` (storeId/productId/search/includeZero/
page/sort). Группы: `ProductGroup` иерархична, есть денормализованный `Path`
(«Родитель/Дочь/Внук») → фильтр по вложенным через префикс Path.
- Штрихкоды: `ProductBarcode(Code, Type:BarcodeType, IsPrimary)`, уникальный индекс
`(OrganizationId, Code)`. Поиск — `GET /api/catalog/products/by-barcode/{value}` и `quick-search`.
- `/api/me` — minimal-endpoint в `Program.cs`: sub/name/email/roles/orgId/hasLiveOrg/
hasActiveEmployee/advancedMode. Прав (RolePermissions) НЕ отдаёт.
- Права: `RolePermissions` (флаги) на роль сотрудника; `PermissionAuthorizationHandler`
(SuperAdmin/Admin → все; иначе роль активного Employee). `RequiresAdvancedMode` гейтит
модули Demands/Transfers/Loyalty/Promotions/PriceTypes/ABC/Audit/Telegram/Presets.
Веб гейтит по именам ролей + `advancedMode` (дублирование — свести к единому источнику).
- POS-синк каталога: `ProductSyncDto` (есть `Packaging` 1/2/3 + `UnitCode`), нужен явный `IsWeighed`.
- Весовой парсер уже есть в `pos.core/Logic/BarcodeParser.cs` (префикс 20–24 вес, 25–29 цена) —
вынести в `food-market.shared`, сделать настраиваемым, использовать и на сервере, и в кассе.
- Миграции — ручные Phase-файлы (raw SQL, IF NOT EXISTS) с `[DbContext]`+`[Migration]`.
## Журнал
- 08.10 — старт; изучена база; план зафиксирован.

77
docs/mobile-api.md Normal file
View file

@ -0,0 +1,77 @@
# API для мобильного «FM Склад»
Серверные возможности, добавленные для iOS-приложения (стенд `api/*`). Авторизация —
OpenIddict password/refresh grant, `client_id=food-market-web`, Bearer-токен. Все
ответы и ошибки — JSON; бизнес-ошибки отдают `{ error, field?, code? }`.
## 1. Остатки по группе товаров (с вложенными)
`GET /api/inventory/stock?groupId={guid}&storeId=&search=&includeZero=&page=&pageSize=&sort=&order=`
- `groupId` — фильтр по группе товаров **включая все вложенные подгруппы**
(по денормализованному `Path`). Комбинируется с `storeId`, `search`, сортировкой.
- Ответ: `PagedResult<StockRow>` — `{ items:[{ productId, productName, article, unitSymbol,
storeId, storeName, quantity, reservedQuantity, available }], total, page, pageSize }`.
## 2. Добавить штрихкод к товару
`POST /api/catalog/products/{id}/barcodes` — право `ProductsEdit`.
Тело: `{ "code": "4870001112223", "kind": "ean13" }`.
`kind` ∈ `ean13|ean8|code128|code39|upca|upce|other` (регистр не важен); если не задан —
тип определяется по длине/цифрам кода.
Ответы:
- `201 Created` → `{ id, productId, code, type, isPrimary }` (первый штрихкод товара
становится primary).
- `200 OK` — тот же код уже у **этого** товара (идемпотентно), тело как у 201.
- `409 Conflict` → `{ error:"Штрихкод уже у товара «…».", code:"barcode-duplicate",
conflictProduct:"…", field:"code" }` — код занят **другим** товаром организации.
- `400` — пустой/слишком длинный код; `404` — товар не найден.
Уникальность проверяется в пределах организации (`(OrganizationId, Code)`).
## 3. Права текущего пользователя
`GET /api/me` теперь возвращает `permissions` — эффективные права (имя флага →
bool) **с учётом роли и простого/расширенного режима**. Мобильное приложение и веб
гейтят UI по одному источнику (скрывают одно и то же).
```json
{
"sub":"…","name":"…","email":"…","roles":["Admin"],"orgId":"…",
"hasLiveOrg":true,"hasActiveEmployee":true,"advancedMode":false,
"permissions":{ "ProductsView":true,"ProductsEdit":true,"StocksView":true,
"DemandsView":false,"TransferEdit":false, … }
}
```
Правила: SuperAdmin — все права без ограничений; Admin — все, но в **простом режиме**
advanced-only скрыты; обычная роль — её флаги (в простом режиме advanced-only тоже
обнуляются). Advanced-only: `DemandsView/Edit/Post`, `TransferEdit`, `LoyaltyManage`,
`PromotionsManage`, `PriceTypesManage` (зеркалит серверный `[RequiresAdvancedMode]`).
Имена флагов — из `RolePermissions`. Веб использует тот же источник (`useCan()`).
## 4. Весовой признак в офлайн-каталоге
`GET /api/pos/v1/sync` (каталог для сканирования/офлайна) в `ProductSyncDto` отдаёт:
- `packaging` (1=штучный, 2=весовой, 3=разливной) и **`isWeighed`** (явный bool `packaging==2`),
- `unitCode` — единица измерения (шт/кг/л…).
## 5. Весовые этикетки (внутримагазинные EAN-13)
Как в МойСкладе: внутримагазинные EAN-13 с префиксом из диапазона 20–29, где зашиты
«код товара» (PLU, N цифр) и величина (вес в граммах → кг, либо цена).
**Настройка организации** — `GET/PUT /api/organization/settings` (PUT — право Admin):
- `weightBarcodeEnabled` (bool, по умолчанию true),
- `weightBarcodeWeightPrefixes` (CSV префиксов веса, null → `20,21,22,23,24`),
- `weightBarcodePricePrefixes` (CSV префиксов цены, null → `25,26,27,28,29`),
- `weightBarcodeCodeDigits` (длина PLU, по умолчанию 5).
Блок применяется, только если в PUT передан `weightBarcodeEnabled` (иначе настройки не трогаются).
**Разбор при поиске** — `GET /api/catalog/products/by-barcode/{value}`:
- Обычный штрихкод → товар (или `{ items:[…] }` при нескольких совпадениях), `404` если нет.
- **Весовая этикетка** (префикс весовой/ценовой) → `{ product:{…}, weighted:true,
valueKind:"WeightKg"|"Price", quantity:1.5, embeddedPrice:null }`. Товар ищется по
встроенному PLU (как короткий штрихкод товара либо как артикул); `quantity` — вес (кг),
`embeddedPrice` — зашитая цена (для ценовых этикеток).
Пример: этикетка `2112345015007` при дефолтных настройках → PLU `12345`, вес `1.500` кг.
Разбор единый для сервера и кассы — `foodmarket.Shared.Barcodes.WeightedBarcode`.