From ad812ee2b58de77e30ed26b4a11aa1d36f5990db Mon Sep 17 00:00:00 2001 From: nns Date: Thu, 8 Oct 2026 05:39:37 +0500 Subject: [PATCH] =?UTF-8?q?docs(mobile-api):=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20API=20=D0=B4=D0=BB=D1=8F=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0=20+=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B3=D1=80=D0=B5=D1=81=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/mobile-api-progress.md | 45 ++++++++++++++++++++++ docs/mobile-api.md | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 docs/mobile-api-progress.md create mode 100644 docs/mobile-api.md diff --git a/docs/mobile-api-progress.md b/docs/mobile-api-progress.md new file mode 100644 index 0000000..53ef2f6 --- /dev/null +++ b/docs/mobile-api-progress.md @@ -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 — старт; изучена база; план зафиксирован. diff --git a/docs/mobile-api.md b/docs/mobile-api.md new file mode 100644 index 0000000..f7bcc41 --- /dev/null +++ b/docs/mobile-api.md @@ -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` — `{ 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`.