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
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:
parent
14001069a2
commit
ad812ee2b5
45
docs/mobile-api-progress.md
Normal file
45
docs/mobile-api-progress.md
Normal 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
77
docs/mobile-api.md
Normal 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`.
|
||||||
Loading…
Reference in a new issue