BR 1.11: Ингредиенты и техкарты

Суть

Перенести ингредиенты из Catalog Service (type=ingredient) в Warehouse Service как отдельную сущность. Обеспечить полноценную работу техкарт: создание рецептов, управление ингредиентами, per-size варианты через structural модификаторы. Себестоимость — Phase 2 (после реализации склада).

Контекст

  • Техкарта = рецепт блюда (состав ингредиентов, нормы расхода, технология приготовления)
  • Сейчас ингредиенты хранятся в products с type=ingredient (ADR-012) — это неправильно, т.к. ингредиенты = складской учёт, не каталог
  • В YumaPos ингредиенты = inventory items, живут отдельно от товаров меню
  • У нас мультитенант: франшиза создаёт стандарт рецепта, франчайзи получает готовый рецепт + будет привязывать к своему складу (Phase 2)

Решения (по итогам аналитического ревью)

#ВопросРешение
1Поля ingredientsid, franchise_id, name, unit_of_measure, status, description, created_at, updated_at
2StatusДа, active/inactive (default active) — для деактивации без удаления
3УдалениеHard delete, но blocked если используется в рецептах (INGREDIENT_IN_USE). Без soft delete.
4unit_of_measureг, кг, мл, л, шт. Конвертация через unit_conversions
5Уникальность имениUNIQUE (franchise_id, name)
6-7МиграцияBig bang в одном релизе. Данных мало (MVP).
9Полуфабрикаты в modifier_tech_cardsНет — только сырые ингредиенты
13-14API request/responseДва поля: ingredient_id и semi_finished_product_id (XOR). В ответе item_type: "ingredient" | "semi_finished"
16Kafka событияНе делаем. Отложено до появления потребителей.
18SidebarНет отдельной вкладки “Ингредиенты”. Создание только inline в контексте техкарты.
21Смена unit_of_measureBlocked если используется (INGREDIENT_IN_USE)
22Unit conversionsДобавить ingredient_id с XOR constraint к unit_conversions

Что нужно

1. Справочник ингредиентов в Warehouse Service

Новая сущность ingredients в Warehouse Service:

ПолеТипОписание
iduuidPK
franchise_iduuidNOT NULL
namevarchar(255)NOT NULL, UNIQUE per franchise
descriptiontextNULL
unit_of_measurevarchar(20)NOT NULL (г, кг, мл, л, шт)
statusvarchar(20)NOT NULL, default ‘active’
created_attimestampNOT NULL
updated_attimestampNOT NULL

API: GET/POST/PATCH/DELETE /api/v1/ingredients

  • GET — поиск (?search=), пагинация, фильтр по статусу
  • POST — создание (Franchise only)
  • PATCH — обновление (блокирует смену unit_of_measure если используется)
  • DELETE — hard delete (блокирует если используется: INGREDIENT_IN_USE)

Создаются франшизой — глобальный справочник для всей сети. Создание только inline в контексте техкарты (отдельного раздела в sidebar нет). Франчайзи/Менеджер — только просмотр.

2. Перевязка техкарт на ingredients

  • recipe_items.ingredient_product_id → разделить на два поля:
    • ingredient_id (FK → ingredients, nullable) — для сырых ингредиентов
    • semi_finished_product_id (uuid, nullable) — для полуфабрикатов (dish из Catalog)
    • CHECK: ровно одно из двух NOT NULL
  • modifier_tech_card_items — только ingredient_id (полуфабрикаты не поддерживаются в modifier tech cards)
  • Миграция: big bang — создать таблицу → скопировать из Catalog → перевязать → удалить старое
  • Catalog Service: убрать type=ingredient из товаров (оставить dish/good)

3. Полуфабрикаты

Полуфабрикат = блюдо (dish) со своей техкартой, используемое как ингредиент в другой техкарте.

  • Ссылка через semi_finished_product_id → Catalog products (cross-service lookup для имени)
  • Циклические ссылки запрещены (валидация при добавлении)
  • Себестоимость разворачивается рекурсивно (Phase 2)

4. Unit conversions

Добавить ingredient_id в unit_conversions с XOR constraint:

  • (product_id IS NOT NULL AND ingredient_id IS NULL) OR (product_id IS NULL AND ingredient_id IS NOT NULL)

5. Фронт

  • Модалка “Добавить ингредиент” в техкарте — поиск по GET /api/v1/ingredients (Warehouse), не по Catalog
  • Кнопка “Создать ингредиент” → быстрая форма (название, единица измерения) → POST /api/v1/ingredients
  • Модалка также показывает блюда (dish) из Catalog для выбора полуфабрикатов
  • BFF proxy routes для /ingredients

6. Что НЕ входит (Phase 2 — склад)

  • Складские остатки (inventory)
  • Приёмки (акты прихода)
  • Средневзвешенная закупочная цена (average_cost)
  • Расчёт себестоимости (CostCalculationService — заработает когда появятся цены)
  • Списание ингредиентов при продаже
  • Авто-стоп по остаткам
  • Kafka события для ингредиентов
  • Отдельный раздел “Ингредиенты” в sidebar

Ролевая модель

РольИнгредиентыТехкарты
ФраншизаCRUD (inline в техкарте)CRUD
ФранчайзиПросмотрПросмотр
Менеджер ТТПросмотрПросмотр
КассирНет доступаНет доступа

Связи

  • Каталог — товары (dish) имеют техкарты; убираем type=ingredient; полуфабрикаты = dish из Catalog
  • Модификаторы — structural определяют per-size техкарты; free имеют свои modifier_tech_cards
  • Прейскуранты — не затрагиваются (ингредиенты не продаются)
  • Склад (Phase 2) — ингредиенты получат average_cost, остатки, приёмки
  • ADR-009, ADR-012 — superseded: ингредиенты переезжают из Catalog в Warehouse