Warehouse Service — API Contract

Этот документ является единственным источником правды для API Warehouse Service.

(Добавлено в BR 1.9)

Содержание

Техкарты

Строки рецепта

Себестоимость

Техкарты модификаторов (BR 1.9.1)

Конвертация единиц

Ингредиенты (BR 1.11)

Склады (BR 1.14)

Складские остатки (BR 1.14)

Акты приёмки (BR 1.14)

Акты списания (BR 1.14)

Средняя цена (BR 1.14)


GET /tech-cards

Список техкарт с фильтрацией по товару.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — активные товары)

Query Parameters

ParamTypeRequiredDescription
product_iduuidnoФильтр по товару
ingredient_iduuidnoФильтр по ингредиенту (полуфабрикат) (BUG-010)
statusstringnoactive / inactive
pageintegernodefault: 1
per_pageintegernodefault: 20, max: 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "product_id": "uuid | null",
      "ingredient_id": "uuid | null",
      "modifier_option_id": "uuid | null",
      "modifier_option_name": "string | null",
      "name": "string",
      "output_weight": "decimal",
      "output_unit": "string",
      "status": "active | inactive",
      "item_count": "integer",
      "created_at": "datetime"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

GET /tech-cards/{id}

Детали техкарты с полным списком ингредиентов.

ПараметрЗначение
AuthBearer JWT (Franchise — любая; Franchisee/Manager — активные товары)

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "product_id": "uuid | null",
    "product_name": "string | null",
    "ingredient_id": "uuid | null",
    "ingredient_name": "string | null",
    "modifier_option_id": "uuid | null",
    "modifier_option_name": "string | null",
    "name": "string",
    "output_weight": "decimal",
    "output_unit": "string",
    "cooking_description": "string | null",
    "status": "active | inactive",
    "items": [
      {
        "id": "uuid",
        "ingredient_id": "uuid",
        "ingredient_name": "string",
        "has_tech_card": "boolean",
        "gross_weight": "decimal",
        "net_weight": "decimal",
        "cold_loss_percent": "decimal | null",
        "hot_loss_percent": "decimal | null",
        "unit_of_measure": "string",
        "sort_order": "integer"
      }
    ],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
TECH_CARD_NOT_FOUND404
TECH_CARD_NOT_FOUND404Техкарта не найдена

POST /tech-cards

Создать техкарту для товара.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "product_id": "uuid, optional — товар из Catalog (для dish-техкарт)",
  "ingredient_id": "uuid, optional — ингредиент из Warehouse (для полуфабрикатов) (BUG-010)",
  "modifier_option_id": "uuid, optional — для per-size",
  "name": "string, required",
  "output_weight": "decimal, required — выход готового",
  "output_unit": "string, required — г | кг | мл | л | порция",
  "cooking_description": "string, optional"
}

Ровно одно из product_id / ingredient_id должно быть указано (BUG-010)

Response 201

Аналогично GET /tech-cards/{id} response (без items — пустая техкарта).

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
VALIDATION_ERROR400Невалидные данные
PRODUCT_NOT_FOUND404Товар не найден в Catalog Service
PRODUCT_NOT_DISH422Товар не типа dish
TECH_CARD_ALREADY_EXISTS409Техкарта для этого товара + модификатора уже существует

PATCH /tech-cards/{id}

Обновить техкарту (поля, не ингредиенты).

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "name": "string, optional",
  "output_weight": "decimal, optional",
  "output_unit": "string, optional",
  "cooking_description": "string | null, optional",
  "status": "string, optional — active | inactive"
}

Response 200

Аналогично GET /tech-cards/{id} response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
TECH_CARD_NOT_FOUND404
TECH_CARD_NOT_FOUND404Техкарта не найдена
VALIDATION_ERROR400

DELETE /tech-cards/{id}

Удалить техкарту.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Response 204

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
TECH_CARD_NOT_FOUND404
TECH_CARD_NOT_FOUND404Техкарта не найдена

POST /tech-cards/{id}/items

Добавить ингредиент в техкарту.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "ingredient_id": "uuid, required — FK → ingredients.id",
  "gross_weight": "decimal, required",
  "net_weight": "decimal, required",
  "cold_loss_percent": "decimal, optional",
  "hot_loss_percent": "decimal, optional",
  "unit_of_measure": "string, required — г | кг | мл | л | шт",
  "sort_order": "integer, optional — default 0"
}
 
### Response 201
 
```json
{
  "data": {
    "id": "uuid",
    "ingredient_id": "uuid",
    "ingredient_name": "string",
    "has_tech_card": "boolean",
    "gross_weight": "decimal",
    "net_weight": "decimal",
    "cold_loss_percent": "decimal | null",
    "hot_loss_percent": "decimal | null",
    "unit_of_measure": "string",
    "sort_order": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
TECH_CARD_NOT_FOUND404
TECH_CARD_NOT_FOUND404Техкарта не найдена
INGREDIENT_NOT_FOUND404Ингредиент не найден
VALIDATION_ERROR400net_weight > gross_weight, отрицательные значения
CIRCULAR_REFERENCE422Ингредиент-полуфабрикат создаёт циклическую ссылку

PATCH /tech-cards/{id}/items/{itemId}

Обновить строку рецепта.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "gross_weight": "decimal, optional",
  "net_weight": "decimal, optional",
  "cold_loss_percent": "decimal, optional",
  "hot_loss_percent": "decimal, optional",
  "unit_of_measure": "string, optional",
  "sort_order": "integer, optional"
}

Response 200

Аналогично POST /tech-cards/{id}/items response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
TECH_CARD_NOT_FOUND404
TECH_CARD_NOT_FOUND404Техкарта не найдена
RECIPE_ITEM_NOT_FOUND404
VALIDATION_ERROR400

DELETE /tech-cards/{id}/items/{itemId}

Удалить строку рецепта.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Response 204

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECIPE_ITEM_NOT_FOUND404

GET /tech-cards/{id}/cost

Расчёт себестоимости техкарты. Разворачивает ингредиенты-полуфабрикаты (имеющие свои техкарты) до сырья.

ПараметрЗначение
AuthBearer JWT (Franchise, Franchisee, Manager)

Response 200

{
  "data": {
    "tech_card_id": "uuid",
    "total_cost": "decimal — общая себестоимость порции",
    "items": [
      {
        "ingredient_id": "uuid",
        "ingredient_name": "string",
        "has_tech_card": "boolean",
        "net_weight": "decimal",
        "unit_of_measure": "string",
        "average_cost_per_unit": "decimal | null",
        "item_cost": "decimal | null"
      }
    ],
    "warnings": [
      "string — например 'Нет данных о цене для Моцарелла'"
    ]
  }
}

Себестоимость

total_cost = ∑(item_cost). Если для ингредиента нет average_costitem_cost = null, ингредиент не входит в сумму, но добавляется в warnings.

Errors

CodeHTTPКогда
UNAUTHORIZED401
TECH_CARD_NOT_FOUND404
TECH_CARD_NOT_FOUND404Техкарта не найдена

GET /modifier-tech-cards

(BR 1.9.1)

Список техкарт модификаторов.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — активные товары)

Query Parameters

ParamTypeRequiredDescription
modifier_option_iduuidnoФильтр по опции

Response 200

{
  "data": [
    {
      "id": "uuid",
      "modifier_option_id": "uuid",
 
      "output_weight": "decimal",
      "output_unit": "string",
      "status": "active | inactive",
      "item_count": "integer"
    }
  ]
}

GET /modifier-tech-cards/{id}

(BR 1.9.1)

Детали техкарты модификатора с ингредиентами.

ПараметрЗначение
AuthBearer JWT (Franchise — любая; Franchisee/Manager — активные товары)

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "modifier_option_id": "uuid",
    "output_weight": "decimal",
    "output_unit": "string",
    "cooking_description": "string | null",
    "status": "active | inactive",
    "items": [
      {
        "id": "uuid",
        "ingredient_id": "uuid",
        "ingredient_name": "string",
        "gross_weight": "decimal",
        "net_weight": "decimal",
        "cold_loss_percent": "decimal | null",
        "hot_loss_percent": "decimal | null",
        "unit_of_measure": "string",
        "sort_order": "integer"
      }
    ],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
MODIFIER_TECH_CARD_NOT_FOUND404

POST /modifier-tech-cards

(BR 1.9.1)

Создать техкарту для опции модификатора.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "modifier_option_id": "uuid, required",
  "output_weight": "decimal, required",
  "output_unit": "string, required",
  "cooking_description": "string, optional"
}

Response 201

Errors

CodeHTTPКогда
MODIFIER_TECH_CARD_ALREADY_EXISTS409Техкарта для этой опции уже существует

PATCH /modifier-tech-cards/{id}

(BR 1.9.1)

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "output_weight": "decimal, optional",
  "output_unit": "string, optional",
  "cooking_description": "string | null, optional",
  "status": "string, optional — active | inactive"
}

Response 200

Аналогично GET /modifier-tech-cards/{id}.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
MODIFIER_TECH_CARD_NOT_FOUND404

DELETE /modifier-tech-cards/{id}

(BR 1.9.1)

ПараметрЗначение
AuthBearer JWT (только Franchise)

Response 204

Errors

CodeHTTPКогда
MODIFIER_TECH_CARD_NOT_FOUND404

POST /modifier-tech-cards/{id}/items

(BR 1.9.1)

Добавить ингредиент в техкарту модификатора.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "ingredient_id": "uuid, required — FK → ingredients.id",
  "gross_weight": "decimal, required",
  "net_weight": "decimal, required",
  "cold_loss_percent": "decimal, optional",
  "hot_loss_percent": "decimal, optional",
  "unit_of_measure": "string, required — г | кг | мл | л | шт",
  "sort_order": "integer, optional — default 0"
}

Response 201

{
  "data": {
    "id": "uuid",
    "ingredient_id": "uuid",
    "ingredient_name": "string",
    "gross_weight": "decimal",
    "net_weight": "decimal",
    "cold_loss_percent": "decimal | null",
    "hot_loss_percent": "decimal | null",
    "unit_of_measure": "string",
    "sort_order": "integer"
  }
}

Errors

CodeHTTPКогда
MODIFIER_TECH_CARD_NOT_FOUND404
INGREDIENT_NOT_FOUND404
CIRCULAR_REFERENCE422

PATCH /modifier-tech-cards/{id}/items/{itemId}

(BR 1.9.1)

Errors

CodeHTTPКогда
MODIFIER_TECH_CARD_NOT_FOUND404
RECIPE_ITEM_NOT_FOUND404

DELETE /modifier-tech-cards/{id}/items/{itemId}

(BR 1.9.1)

Response 204

Errors

CodeHTTPКогда
RECIPE_ITEM_NOT_FOUND404

GET /unit-conversions

Конвертации единиц для продукта.

ПараметрЗначение
AuthBearer JWT (Franchise)

Query Parameters

ParamTypeRequiredDescription
product_iduuidyesПродукт, для которого список конвертаций

Response 200

{
  "data": [
    {
      "id": "uuid",
      "product_id": "uuid",
      "from_unit": "string",
      "to_unit": "string",
      "factor": "decimal"
    }
  ]
}

POST /unit-conversions

Создать конвертацию.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "product_id": "uuid, required",
  "from_unit": "string, required",
  "to_unit": "string, required",
  "factor": "decimal, required — > 0"
}

Response 201

{
  "data": {
    "id": "uuid",
    "product_id": "uuid",
    "from_unit": "string",
    "to_unit": "string",
    "factor": "decimal"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
VALIDATION_ERROR400
CONVERSION_ALREADY_EXISTS409

PATCH /unit-conversions/{id}

Обновить коэффициент конвертации.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "factor": "decimal, required — > 0"
}

Response 200

Аналогично POST /unit-conversions response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
CONVERSION_NOT_FOUND404

GET /ingredients

(BR 1.11)

Список ингредиентов франшизы.

ПараметрЗначение
AuthBearer JWT (Franchise, Franchisee, Manager; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
searchstringnoПоиск по названию
statusstringnoФильтр: active / inactive
pageintegernodefault: 1
per_pageintegernodefault: 20, max: 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "franchise_id": "uuid",
      "name": "string",
      "description": "string | null",
      "unit_of_measure": "string",
      "status": "active | inactive",
      "created_at": "datetime",
      "updated_at": "datetime"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль Cashier

GET /ingredients/{id}

(BR 1.11)

Детали ингредиента.

ПараметрЗначение
AuthBearer JWT (Franchise, Franchisee, Manager; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID ингредиента

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "name": "string",
    "description": "string | null",
    "unit_of_measure": "string",
    "status": "active | inactive",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль Cashier
INGREDIENT_NOT_FOUND404Ингредиент не найден

POST /ingredients

(BR 1.11)

Создать ингредиент.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "name": "string, required — название ингредиента",
  "description": "string, optional — описание",
  "unit_of_measure": "string, required — г | кг | мл | л | шт"
}

Response 201

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "name": "string",
    "description": "string | null",
    "unit_of_measure": "string",
    "status": "active",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
VALIDATION_ERROR400Невалидные данные
NAME_DUPLICATE409Ингредиент с таким названием уже существует

PATCH /ingredients/{id}

(BR 1.11)

Обновить ингредиент.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID ингредиента

Request Body

{
  "name": "string, optional",
  "description": "string | null, optional",
  "unit_of_measure": "string, optional — г | кг | мл | л | шт",
  "status": "string, optional — active | inactive"
}

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "name": "string",
    "description": "string | null",
    "unit_of_measure": "string",
    "status": "active | inactive",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
INGREDIENT_NOT_FOUND404Ингредиент не найден
NAME_DUPLICATE409Ингредиент с таким названием уже существует
INGREDIENT_IN_USE422Нельзя изменить unit_of_measure — ингредиент используется в техкартах

DELETE /ingredients/{id}

(BR 1.11. Обновлено в BUG-024)

Удалить ингредиент.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID ингредиента

Поведение

  • Если ингредиент используется как компонент в чужих техкартах/модификаторах (recipe_items.ingredient_id или modifier_tech_card_items.ingredient_id) — 422 INGREDIENT_IN_USE
  • Если у ингредиента есть собственная техкарта (ингредиент-полуфабрикат: tech_cards.ingredient_id = :id) — техкарта и её recipe_items каскадно удаляются вместе с ингредиентом в одной транзакции. Это ожидаемое поведение: техкарта принадлежит своему полуфабрикату.

Response 204

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
INGREDIENT_NOT_FOUND404Ингредиент не найден
INGREDIENT_IN_USE422Ингредиент используется как компонент в чужих техкартах/модификаторах
DB_CONSTRAINT409Safety net: FK-ограничение БД не обработано на уровне сервиса

GET /warehouses

(BR 1.14)

Список складов франшизы. Фильтрация по доступу: Franchise — все, Franchisee/Manager — свои store_ids.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee — свои; Manager — свой; Cashier — 403)

Response 200

{
  "data": [
    {
      "id": "uuid",
      "franchise_id": "uuid",
      "store_id": "uuid",
      "name": "string",
      "status": "active | inactive",
      "created_at": "datetime"
    }
  ]
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль Cashier

POST /warehouses

(BR 1.14)

Создать склад вручную (обычно склад создаётся автоматически при создании ТТ в Store Service через internal-вызов). Полезно для стандартизированного создания склада с конкретным store_id и name.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "store_id": "uuid, required — ID торговой точки",
  "name": "string, required — название склада"
}

Response 201

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "store_id": "uuid",
    "name": "string",
    "status": "active",
    "created_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль не Franchise
STORE_NOT_FOUND404Store не найден
WAREHOUSE_ALREADY_EXISTS409Склад для этой ТТ уже создан
VALIDATION_ERROR400Пустое name / невалидный store_id

POST /internal/warehouses

(BR 1.14)

Internal endpoint для авто-создания склада из Store Service в момент создания ТТ. Идемпотентный — повторный вызов с теми же store_id + franchise_id вернёт существующий склад с 200 OK, без 409.

ПараметрЗначение
AuthX-Service-Token header
Content-Typeapplication/json
Path/internal/warehouses (без /api/v1 префикса)

Request Body

{
  "store_id": "uuid, required",
  "franchise_id": "uuid, required",
  "name": "string, required — обычно name ТТ"
}

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "store_id": "uuid",
    "name": "string",
    "status": "active",
    "created_at": "datetime"
  }
}

Best-effort вызов из Store Service

StoreService.create() вызывает этот эндпоинт после storeRepository.save(store). При HTTP-ошибке Store Service логирует warning, но не откатывает создание ТТ — bootstrap может быть подтянут позже через backfill-эндпоинт.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный/отсутствующий X-Service-Token
VALIDATION_ERROR400Пустое name / невалидный UUID

POST /internal/warehouses/backfill

(BR 1.14)

Internal endpoint для одноразового подтягивания складов под уже существующие ТТ. Дёргает Store Service GET /internal/stores/all, для каждой ТТ вызывает ensureForStore — создаёт склад если ещё нет, иначе ничего.

ПараметрЗначение
AuthX-Service-Token header
Path/internal/warehouses/backfill

Response 200

{
  "data": {
    "total_stores": 12,
    "created": 7,
    "skipped": 5
  }
}

Используется один раз после деплоя

Идемпотентен — повторные вызовы не приведут к дубликатам, но это не часть runtime-flow. Когда auto-create через Store Service работает для всех новых ТТ, backfill больше не нужен.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный X-Service-Token

GET /stock-balances

(BR 1.14)

Список складских остатков с фильтрацией.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — свои store_ids; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
warehouse_iduuidyesСклад, остатки которого запрашиваем
ingredient_iduuidnoФильтр по ингредиенту
pageintegernodefault: 1
per_pageintegernodefault: 20, max: 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "warehouse_id": "uuid",
      "warehouse_name": "string",
      "ingredient_id": "uuid",
      "ingredient_name": "string",
      "current_quantity": "decimal",
      "average_cost": "decimal | null",
      "unit_of_measure": "string",
      "updated_at": "datetime"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль Cashier или нет доступа к складу

GET /stock-balances/{warehouseId}/{ingredientId}

(BR 1.14)

Остаток конкретного ингредиента на конкретном складе.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
warehouseIduuidyesID склада
ingredientIduuidyesID ингредиента

Response 200

{
  "data": {
    "id": "uuid",
    "warehouse_id": "uuid",
    "warehouse_name": "string",
    "ingredient_id": "uuid",
    "ingredient_name": "string",
    "current_quantity": "decimal",
    "average_cost": "decimal | null",
    "unit_of_measure": "string",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
STOCK_BALANCE_NOT_FOUND404Нет баланса для этого ингредиента на складе

GET /receipt-acts

(BR 1.14)

Список актов приёмки с фильтрацией.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — свои склады; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
warehouse_iduuidnoФильтр по складу
statusstringnodraft / posted
pageintegernodefault: 1
per_pageintegernodefault: 20, max: 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "franchise_id": "uuid",
      "warehouse_id": "uuid",
      "warehouse_name": "string",
      "document_number": "string",
      "receipt_date": "datetime",
      "comment": "string | null",
      "status": "draft | posted",
      "total_amount": "decimal",
      "created_by": "uuid",
      "created_at": "datetime",
      "updated_at": "datetime"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль Cashier или нет доступа к складу

POST /receipt-acts

(BR 1.14)

Создать акт приёмки (draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой склад; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Request Body

{
  "warehouse_id": "uuid, required — склад",
  "receipt_date": "datetime, required — дата приёмки",
  "comment": "string, optional — комментарий"
}

Response 201

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "warehouse_id": "uuid",
    "warehouse_name": "string",
    "document_number": "string",
    "receipt_date": "datetime",
    "comment": "string | null",
    "status": "draft",
    "total_amount": "decimal",
    "lines": [],
    "created_by": "uuid",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Нет доступа к складу
VALIDATION_ERROR400Невалидные данные
WAREHOUSE_NOT_FOUND404Склад не найден

GET /receipt-acts/{id}

(BR 1.14)

Детали акта приёмки со строками.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта приёмки

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "warehouse_id": "uuid",
    "warehouse_name": "string",
    "document_number": "string",
    "receipt_date": "datetime",
    "comment": "string | null",
    "status": "draft | posted",
    "total_amount": "decimal",
    "lines": [
      {
        "id": "uuid",
        "ingredient_id": "uuid",
        "ingredient_name": "string",
        "quantity": "decimal",
        "unit_of_measure": "string",
        "unit_price": "decimal",
        "line_total": "decimal",
        "shelf_life_date": "date | null"
      }
    ],
    "created_by": "uuid",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECEIPT_ACT_NOT_FOUND404Акт приёмки не найден

PATCH /receipt-acts/{id}

(BR 1.14)

Обновить акт приёмки (только в статусе draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта приёмки

Request Body

{
  "receipt_date": "datetime, optional",
  "comment": "string | null, optional"
}

Response 200

Аналогично GET /receipt-acts/{id} response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECEIPT_ACT_NOT_FOUND404Акт не найден
DOCUMENT_ALREADY_POSTED422Документ уже проведён, редактирование запрещено

POST /receipt-acts/{id}/post

(BR 1.14)

Провести акт приёмки. Создаёт партии, обновляет остатки, пересчитывает среднюю цену.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта приёмки

Response 200

Аналогично GET /receipt-acts/{id} response (status = posted).

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECEIPT_ACT_NOT_FOUND404
DOCUMENT_ALREADY_POSTED422Документ уже проведён
EMPTY_DOCUMENT422Нет строк в документе

POST /receipt-acts/{id}/lines

(BR 1.14)

Добавить строку в акт приёмки (только draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта приёмки

Request Body

{
  "ingredient_id": "uuid, required — FK → ingredients.id",
  "quantity": "decimal, required — > 0",
  "unit_of_measure": "string, required — г | кг | мл | л | шт",
  "unit_price": "decimal, required — >= 0, закупочная цена",
  "shelf_life_date": "date, optional — срок годности"
}

Response 201

{
  "data": {
    "id": "uuid",
    "ingredient_id": "uuid",
    "ingredient_name": "string",
    "quantity": "decimal",
    "unit_of_measure": "string",
    "unit_price": "decimal",
    "line_total": "decimal",
    "shelf_life_date": "date | null"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECEIPT_ACT_NOT_FOUND404
INGREDIENT_NOT_FOUND404Ингредиент не найден
DOCUMENT_ALREADY_POSTED422Документ уже проведён
VALIDATION_ERROR400Невалидные данные

PATCH /receipt-acts/{id}/lines/{lineId}

(BR 1.14)

Обновить строку акта приёмки (только draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта приёмки
lineIduuidyesID строки

Request Body

{
  "quantity": "decimal, optional — > 0",
  "unit_of_measure": "string, optional",
  "unit_price": "decimal, optional — >= 0",
  "shelf_life_date": "date | null, optional"
}

Response 200

Аналогично POST /receipt-acts/{id}/lines response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECEIPT_ACT_NOT_FOUND404
RECEIPT_ACT_LINE_NOT_FOUND404Строка не найдена
DOCUMENT_ALREADY_POSTED422

DELETE /receipt-acts/{id}/lines/{lineId}

(BR 1.14)

Удалить строку из акта приёмки (только draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта приёмки
lineIduuidyesID строки

Response 200

Возвращает обновлённый объект ReceiptActResponse (полный акт со всеми оставшимися строками и пересчитанными итогами).

{ "data": { "id": "uuid", "status": "draft", "lines": [...], "total": "decimal" } }

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
RECEIPT_ACT_NOT_FOUND404
RECEIPT_ACT_LINE_NOT_FOUND404
DOCUMENT_ALREADY_POSTED422

GET /write-off-acts

(BR 1.14)

Список актов списания с фильтрацией.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — свои склады; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
warehouse_iduuidnoФильтр по складу
statusstringnodraft / posted
pageintegernodefault: 1
per_pageintegernodefault: 20, max: 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "franchise_id": "uuid",
      "warehouse_id": "uuid",
      "warehouse_name": "string",
      "document_number": "string",
      "write_off_date": "datetime",
      "reason": "string",
      "status": "draft | posted",
      "total_cost": "decimal",
      "created_by": "uuid",
      "created_at": "datetime",
      "updated_at": "datetime"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль Cashier или нет доступа к складу

POST /write-off-acts

(BR 1.14)

Создать акт списания (draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой склад; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Request Body

{
  "warehouse_id": "uuid, required — склад",
  "write_off_date": "datetime, required — дата списания",
  "reason": "string, required — причина списания (порча, недостача и т.д.)"
}

Response 201

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "warehouse_id": "uuid",
    "warehouse_name": "string",
    "document_number": "string",
    "write_off_date": "datetime",
    "reason": "string",
    "status": "draft",
    "total_cost": "decimal",
    "lines": [],
    "created_by": "uuid",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Нет доступа к складу
VALIDATION_ERROR400Невалидные данные
WAREHOUSE_NOT_FOUND404Склад не найден

GET /write-off-acts/{id}

(BR 1.14)

Детали акта списания со строками.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта списания

Response 200

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "warehouse_id": "uuid",
    "warehouse_name": "string",
    "document_number": "string",
    "write_off_date": "datetime",
    "reason": "string",
    "status": "draft | posted",
    "total_cost": "decimal",
    "lines": [
      {
        "id": "uuid",
        "ingredient_id": "uuid",
        "ingredient_name": "string",
        "quantity": "decimal",
        "unit_cost": "decimal",
        "line_cost": "decimal"
      }
    ],
    "created_by": "uuid",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
WRITE_OFF_ACT_NOT_FOUND404Акт списания не найден

PATCH /write-off-acts/{id}

(BR 1.14)

Обновить акт списания (только draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта списания

Request Body

{
  "write_off_date": "datetime, optional",
  "reason": "string, optional"
}

Response 200

Аналогично GET /write-off-acts/{id} response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
WRITE_OFF_ACT_NOT_FOUND404Акт не найден
DOCUMENT_ALREADY_POSTED422Документ уже проведён

POST /write-off-acts/{id}/post

(BR 1.14)

Провести акт списания. FIFO-списание с партий, обновление остатков.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта списания

Response 200

Аналогично GET /write-off-acts/{id} response (status = posted).

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
WRITE_OFF_ACT_NOT_FOUND404
DOCUMENT_ALREADY_POSTED422Документ уже проведён
EMPTY_DOCUMENT422Нет строк в документе
INSUFFICIENT_STOCK422Недостаточно остатков на складе

POST /write-off-acts/{id}/lines

(BR 1.14)

Добавить строку в акт списания (только draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта списания

Request Body

{
  "ingredient_id": "uuid, required — FK → ingredients.id",
  "quantity": "decimal, required — > 0, количество к списанию"
}

unit_cost и line_cost рассчитываются автоматически из средневзвешенной цены ингредиента на складе.

Response 201

{
  "data": {
    "id": "uuid",
    "ingredient_id": "uuid",
    "ingredient_name": "string",
    "quantity": "decimal",
    "unit_cost": "decimal",
    "line_cost": "decimal"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
WRITE_OFF_ACT_NOT_FOUND404
INGREDIENT_NOT_FOUND404Ингредиент не найден
DOCUMENT_ALREADY_POSTED422
VALIDATION_ERROR400Невалидные данные

DELETE /write-off-acts/{id}/lines/{lineId}

(BR 1.14)

Удалить строку из акта списания (только draft).

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID акта списания
lineIduuidyesID строки

Response 204

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
WRITE_OFF_ACT_NOT_FOUND404
WRITE_OFF_ACT_LINE_NOT_FOUND404
DOCUMENT_ALREADY_POSTED422

GET /ingredients/{id}/average-cost

(BR 1.14)

Средневзвешенная закупочная цена ингредиента. Рассчитывается из складских партий.

ПараметрЗначение
AuthBearer JWT (Franchise — любой склад; Franchisee/Manager — свой склад; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID ингредиента

Query Parameters

ParamTypeRequiredDescription
warehouse_iduuidyesСклад, для которого считаем среднюю цену

Response 200

{
  "data": {
    "ingredient_id": "uuid",
    "ingredient_name": "string",
    "warehouse_id": "uuid | null",
    "average_cost": "decimal | null",
    "unit_of_measure": "string",
    "total_quantity": "decimal"
  }
}

Если нет партий с количеством > 0, average_cost = null.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
INGREDIENT_NOT_FOUND404Ингредиент не найден

Общий формат ошибок

{
  "error": {
    "code": "string — UPPER_SNAKE_CASE",
    "message": "string — человекочитаемое",
    "details": [
      {
        "field": "string",
        "message": "string"
      }
    ]
  }
}