Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
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 по одному источнику (скрывают одно и то же).
{
"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(явный boolpackaging==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— вернуть в черновик. Поведение изменено (08.10): если по инвентаризации есть проведённые связанные Оприходование/ Списание — возвращает 409 с текстом «По инвентаризации проведены Оприходование О-… и Списание С-…. Сначала отмените результаты» и полямиenters/losses(id+номер). Больше НЕ удаляет проведённые документы молча. Если связанные документы только черновики (или их нет) — удаляет черновики (они не влияли на остатки) и возвращает инвентаризацию в черновик (204).POST /api/inventory/inventories/{id}/cancel-results— отменить результаты: снимает проведённые связанные Оприходование/Списание с проведения (остатки возвращаются), НЕ удаляя их — они становятся черновиками и остаются в «Связанных документах», истории и аудите. Право:InventoryEdit+EnterEdit(если есть проведённые Оприходования) и/илиLossEdit(если есть Списания). 409, если снятие Оприходования уведёт остаток в минус (часть излишка уже продана) — с полемlines. После отмены результатов инвентаризацию можно вернуть в черновик (unpost) или снова провести документы (enters/{id}/post,losses/{id}/post).
Статус: временное. Рекомендуемый путь для новых версий мобильного приложения —
complete + create-enter/create-loss (как в вебе), а для отмены — cancel-results
вместо удаления, чтобы история и аудит сохранялись. post/unpost будут помечены
deprecated после обновления iOS (iOS обновит Mac-Claude).
Отрицательные остатки: предупреждения + push (09.10)
Продажа/списание/перемещение/отгрузка/возврат/отмена приёмки в минус разрешены (решение владельца), но создают предупреждение об отрицательном остатке. iOS показывает счётчик/список и может запустить пересчёт.
GET /api/negative-stock/count[?storeId=]→{ total, byStore: [{storeId, storeName, count}] }. Для баннера/индикатора «N товаров в минусе».GET /api/negative-stock[?storeId=&overdueHours=24]→ список открытых предупреждений:{ id, storeId, storeName, productId, productName, quantity(<0), causeDocType, causeDocNumber, causeUserId, causeAt, firstDetectedAt, lastUpdatedAt, overdue(bool), hint }.hint— подсказка причины, напр. «возможно, не проведена приёмка П-… от <поставщик>».POST /api/negative-stock/recountbody{ storeId }→ создаёт черновик инвентаризации по складу ровно с минусовыми товарами (BookQty = текущий отрицательный остаток) →{ 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-tokensbody{ platform: "ios"|"android"|"web", token }— регистрация токена (идемпотентно по токену).DELETE /api/me/device-tokens?token=…— отзыв.- Реальная отправка push пока выключена (нужен платный Apple Developer): в DI включён
StubPushSender(пишет в лог). Контракт —IPushSender.SendAsync(userIds, title, body, data); когда появятся ключи APNs/FCM/VAPID, заглушка подменяется реальной реализацией без правок вызывающего кода. Фоновая агрегированная рассылка предупреждений (одно сообщение на склад, тихие часы, получатели — Админ+Кладовщики склада) и эскалация — отдельный проход.