BR 2.3 (draft) — Статусная модель заказа: Yuma-паритет

Это research-заметка, не финальная спека

Записано «на подумать» — собрано из исследования YumaPOS и аудита текущего кода Order Service. Возвращаться к ней после обсуждения приоритетов.


1. Контекст

Текущая статусная модель Order Service работает, но устарела относительно спеки (03-Services/Order Service/Data Model.md) и не покрывает реальных кассовых сценариев. Разница с юмой (наш референс) — в степени детализации flow.

Проверено:

  • Сейчас все заказы (takeaway / dine_in / delivery) идут по одному и тому же flow: new → accepted → ready → closed
  • Быстрая продажа (клиент попросил бутылку воды) вынужденно проходит «в работу» → «готов», хотя по факту готовить нечего
  • Нет явной кнопки «Начать готовку» — статус двигается косвенно, внутренними переходами
  • OrderItem не имеет status — кухня не может отмечать позиции по отдельности

2. Что удалось выяснить про Yuma

2.1. Статусы (см. process-orders)

РусскийАнглийский в нашей моделиСмысл
НовыйnewСоздан, в работу не ушёл
В процессеin_progressНа кухне/собирается
Готов / ВыполненreadyВсе позиции готовы, ждёт выдачи
ЗакрытclosedОплачен + выдан клиенту
ОтменёнcancelledОтменён до оплаты

2.2. Типы заказа определяют flow

ТипFlowКухня
Быстрый заказ (quick / counter-sale)Новый → ЗакрытНет
За столом (dine-in)Новый → В процессе → Готов → ЗакрытДа, по явной кнопке
Доставка (delivery)Новый → В процессе → Готов → Передан водителю → Доставляется → Доставлено → ЗакрытДа
Автокафе (drive-through)Как dine-inДа
ГрупповойКак dine-inДа

Ключевое: тип выбирается до добавления товаров, не на чекауте.

2.3. Кухня включается явной кнопкой «Начать»

Для dine-in/delivery переход Новый → В процессе триггерится нажатием официанта/кассира. Не чекаутом. Сам факт оплаты не равен началу готовки.

Если за столом попросили только воду (готовить нечего) — официант может пропустить «Начать», сразу принять оплату, заказ идёт короткой дорогой Новый → Закрыт.

2.4. Item-level статусы

У юмы каждая позиция в заказе имеет собственный статус: В процессеГотов. Когда все позиции Готов, заказ автоматически → Готов. Кухонный экран (KDS) показывает именно позиции, а не заказ целиком.

2.5. Принтеры/станции на уровне товара

У каждого товара — поле «Принтеры»: на какой кухонный/барный принтер уедет чек. Если у товара нет принтера — он не печатается никуда, но висит в заказе (для официанта). Это и есть механизм «заказ с водой не идёт на кухню».

2.6. Paid ≠ Closed

Юма разделяет оплату и выдачу:

  • Оплачено — зелёная иконка у суммы, статус остаётся Ready
  • Закрыт — клиент забрал товар, заказ финализирован
  • Есть настройка «автозакрытие через N сек после оплаты» per-терминал — опционально

2.7. Delivery substatus-chain

Для доставочных заказов — отдельная цепочка поверх основного статуса:

Ready → Передан водителю (handed_over) → Доставляется (in_delivery) → Доставлено (delivered) → Закрыт

Каждый substatus — отдельное событие в event bus, курьерское приложение меняет их по мере выполнения.

2.8. Refund vs Cancel — строгое разделение

  • Cancel — до оплаты. Клик «Отменить» возможен только в Новый или В процессе (неоплаченный)
  • Refund — после оплаты. Отдельная сущность, возврат строго тем же способом оплаты (card→card, cash→cash, online→card)
  • Permissions разные: Cancel Order vs Return

2.9. Split-payment для dine-in

В dine-in можно разделить оплату по стульям: «этот стул платит картой, этот — наличкой». Сущности seat и payment_line связаны с order.


3. Что у нас сейчас (аудит erp-order-service)

Что есть

  • ✅ Основная цепочка new → accepted → ready → closed/cancelled
  • accepted как русский аналог «В процессе» (на уровне неймингов — расхождение со спекой, но фактически это in_progress)
  • handed_over для аггрегаторных заказов — аналог первого шага delivery-substatus
  • ✅ Поле order_type (takeaway / dine_in / delivery)
  • ✅ Поле channel (INTERNAL / агрегаторы)
  • ✅ Поля paid_at и completed_at отдельно — технически позволяют разделить paid и closed
  • RefundRecord + RefundRequest — возвраты отдельно от отмены
  • refund_total, статусы refund_request (pending/confirmed/rejected/expired)
  • ✅ Миграция 005-add-aggregator-statuses.xml — расширила CHECK до включения accepted и handed_over

Чего нет

  • Item-level статусов. OrderItem не имеет поля status. Кухня может только двигать весь заказ целиком.
  • Зависимости flow от типа заказа. takeaway проходит все статусы, как и dine_in. Быстрая продажа засоряет кухонный queue.
  • Аналога «Начать» как явного жеста. Переходы new → accepted делаются внутренней логикой, не кнопкой кассира.
  • Полной delivery substatus-chain. handed_over есть, но in_delivery / delivered нет.
  • Paid ≠ Closed разделения. POS-checkout склеивает оплату и закрытие в одном вызове.
  • Автозакрытия по таймеру.
  • Split payment по стульям. Dine-in работает, но оплата одна на весь заказ. Нет seat_id / payment_line.
  • Кухонных станций / принтеров. У Product нет поля типа kitchen_station_id / requires_kitchen. Нельзя сказать «этот товар на кухню не идёт».
  • Actual KDS-экрана с item-level отметками. В erp-pos/mobile есть KitchenQueueScreen, но он показывает заказы целиком без детализации по позициям.

Расхождение кода со спекой

  • Спека Data Model.md строка 88 описывает статусы как new / in_progress / ready / closed / cancelled
  • Код пишет new / accepted / ready / handed_over / closed / cancelled
  • Миграция 005 легализовала фактическое положение (CHECK allows both)
  • Переименовывать accepted → in_progress сейчас рискованно: 5+ мест в erp-admin/web (STATUS_LABEL, filters), 5+ мест в erp-pos/mobile (KitchenQueueScreen, AggregatorOrdersScreen), типы TS, живые данные в БД на test-VPS
  • Рекомендация (безопасная): актуализировать спеку под код — добавить accepted и handed_over как валидные статусы, описать контекст (aggregator flow)

4. Гипотезы что делать (варианты)

Вариант А: минимальная актуализация

Только обновить спеку Data Model.md под текущий код. Без изменения логики. Закрывает документационный долг, но не добавляет фич.

Вариант Б: разделить flow по типу заказа

Добавить правило: takeaway без кухонных позиций → автосокращение до new → closed. dine_in / delivery — полный flow с явным «Начать». Потребует:

  • Поле Product.requires_kitchen (catalog-service)
  • Правило в OrderService.checkout
  • Кнопка «Начать готовку» в POS-mobile

Вариант В: полный Yuma-паритет

Всё из Б + item-level статусы + delivery substatus-chain + split-payment + paid≠closed + автозакрытие. Это серия отдельных BR, каждая — заметная работа с участием POS-фронта и админки.


5. Открытые вопросы

  • Нужно ли split-payment по стульям в MVP? Большинство российских кофеен и фастфуда не делят чек по стульям. Может быть out of scope Phase 1.
  • Кухонные станции / принтеры — это поле на Product или отдельная сущность «кухонная зона»? Юма использует просто список принтеров, привязанных к товару.
  • Paid ≠ Closed — нужен ли нам визуально «оплачен, но не выдан»? В небольших кофейнях это малоприменимо (клиент оплачивает и сразу получает), но в ресторане за столом — да.
  • KDS как отдельное приложение или интеграция в существующий POS-mobile? У юмы — отдельное приложение для кухни.
  • Автозакрытие таймером — per-терминал настройка или глобальная? Юма делает per-терминал, это значит нужно поле в stores или pos_terminals.

6. Следующие шаги (когда вернёмся)

  1. Решить скоуп: A / Б / В или их комбинация
  2. Если Б/В — декомпозировать на отдельные BR (по одной фиче: item-статусы, delivery-chain, split-payment, paid≠closed)
  3. Обновить Data Model.md Order Service (безопасная часть)
  4. Добавить Product.requires_kitchen в BR каталога (catalog-service) — предусловие для разделения flow

7. Ссылки