# 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`. ## Инвентаризация: post/unpost (временная совместимость) С переходом на модель МойСклада инвентаризация сама остатки **не двигает** (см. `docs/inventory-moysklad-reference.md`). Веб использует: `complete` → `create-enter` / `create-loss`, затем проводит документы. Мобильное приложение «FM Склад» исторически вызывает `post`/`unpost` инвентаризации. Чтобы не ломать установленные приложения, эндпоинты **оставлены как совместимые**: - `POST /api/inventory/inventories/{id}/post` — **= завершить + создать и ПРОВЕСТИ** Оприходование (излишки) и Списание (недостача) одним вызовом. Идемпотентно: если связанные документы уже созданы — не дублирует. - `POST /api/inventory/inventories/{id}/unpost` — отменить результаты: отменить проведение и удалить связанные Оприходование/Списание (остатки вернутся), вернуть инвентаризацию в черновик. **Статус: временное.** Рекомендуемый путь для новых версий мобильного приложения — `complete` + `create-enter`/`create-loss` (как в вебе), чтобы кладовщик мог проверить документы перед проведением. `post`/`unpost` будут помечены deprecated после обновления iOS.