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 | Поля ingredients | id, franchise_id, name, unit_of_measure, status, description, created_at, updated_at |
| 2 | Status | Да, active/inactive (default active) — для деактивации без удаления |
| 3 | Удаление | Hard delete, но blocked если используется в рецептах (INGREDIENT_IN_USE). Без soft delete. |
| 4 | unit_of_measure | г, кг, мл, л, шт. Конвертация через unit_conversions |
| 5 | Уникальность имени | UNIQUE (franchise_id, name) |
| 6-7 | Миграция | Big bang в одном релизе. Данных мало (MVP). |
| 9 | Полуфабрикаты в modifier_tech_cards | Нет — только сырые ингредиенты |
| 13-14 | API request/response | Два поля: ingredient_id и semi_finished_product_id (XOR). В ответе item_type: "ingredient" | "semi_finished" |
| 16 | Kafka события | Не делаем. Отложено до появления потребителей. |
| 18 | Sidebar | Нет отдельной вкладки “Ингредиенты”. Создание только inline в контексте техкарты. |
| 21 | Смена unit_of_measure | Blocked если используется (INGREDIENT_IN_USE) |
| 22 | Unit conversions | Добавить ingredient_id с XOR constraint к unit_conversions |
Что нужно
1. Справочник ингредиентов в Warehouse Service
Новая сущность ingredients в Warehouse Service:
| Поле | Тип | Описание |
|---|---|---|
id | uuid | PK |
franchise_id | uuid | NOT NULL |
name | varchar(255) | NOT NULL, UNIQUE per franchise |
description | text | NULL |
unit_of_measure | varchar(20) | NOT NULL (г, кг, мл, л, шт) |
status | varchar(20) | NOT NULL, default ‘active’ |
created_at | timestamp | NOT NULL |
updated_at | timestamp | NOT 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