Заказы
Источники требований
- BR 2.1 — Order Service — базовая модель
- BR 3.1 — привязка клиента к заказу
- BR 2.5 — расширенная статусная модель, Paid≠Closed, flow по типу заказа, delivery substatus, кухонные станции, webhook-подписки
- BR 5.1 —
kitchen_statusper-item,expected_ready_at, KDS-flow
Заказы — ядро бизнеса. Order Service отвечает за создание заказов, управление позициями, статусную машину и фиксацию факта оплаты. POS-терминал (или внешний POS через API) проводит платёж и вызывает API Order Service.
Сервис: Order Service (:3005).
Скоуп после BR 2.5: реализованы все три типа — takeaway, dine_in, delivery. Статусная модель расширена (accepted, handed_over, in_delivery, delivered). Оплата разделена с закрытием (paid_at отдельно от closed). Кухонный flow включается явным вызовом «Начать готовку» + зависит от наличия в заказе товаров с requires_kitchen=true.
Заказ (order)
Поля
| Поле | Обязательность | Описание |
|---|---|---|
| Франшиза | Обязательно | franchise_id (из JWT) |
| Торговая точка | Обязательно | store_id (кросс-сервисная ссылка → Store Service) |
| Номер заказа | Обязательно | Автоинкремент per-store per-day: “001”, “002”… Сбрасывается каждый день |
| Тип заказа | Обязательно | takeaway / dine_in / delivery — определяет flow статусов (BR 2.5) |
| Статус | Обязательно | new / accepted / ready / handed_over / in_delivery / delivered / closed / cancelled. Нейминг accepted устоялся в коде (исторически) = семантически «В процессе / на кухне». (Расширено в BR 2.5) |
Требует кухни (requires_kitchen) | Обязательно | Boolean, рассчитывается на checkout из позиций (true если хотя бы один товар с requires_kitchen=true). Определяет flow: true → полный flow с кухней; false + takeaway → сокращённый new → closed. (Добавлено в BR 2.5) |
| Итого | Обязательно | Сумма всех позиций (пересчитывается при добавлении/удалении) |
| Способ оплаты | Необязательно | cash / card / qr / mixed / NULL (до оплаты) |
| Сумма оплаты | Необязательно | Фактическая сумма оплаты |
| RRN | Необязательно | Для карточных платежей (от PBF) |
| Последние 4 цифры карты | Необязательно | Для карточных платежей |
| Комментарий | Необязательно | Произвольный текст |
| Причина отмены | Необязательно | Заполняется при отмене |
| Клиент | Необязательно | FK → Клиенты. Заказ может быть анонимным (без клиента) или именным. Прикрепляется кассиром на POS через поиск по телефону. (Добавлено в BR 3.1) |
| Создал | Обязательно | user_id кассира/менеджера (из JWT) |
| Дата создания | Обязательно | |
| Дата оплаты | Необязательно | Проставляется при фиксации оплаты |
| Дата завершения | Необязательно | Проставляется при закрытии (выдаче) |
| Дата отмены | Необязательно | Проставляется при отмене |
Плановое время готовности (expected_ready_at) | Необязательно | Время на которое заказ должен быть готов. По умолчанию = accepted_at + avg_prep_time(station) (или 15 мин если средний срок неизвестен). Используется KDS для расчёта цвета карточки и звука просрочки. (Добавлено в BR 5.1) |
Позиция заказа (order_item)
Поля
| Поле | Обязательность | Описание |
|---|---|---|
| Заказ | Обязательно | FK → orders |
| Товар (ID) | Обязательно | product_id (кросс-сервисная ссылка → Catalog Service) |
| Товар (название) | Обязательно | Денормализовано — копируется при создании позиции |
| Количество | Обязательно | |
| Цена за единицу | Обязательно | Денормализовано — копируется из прейскуранта на момент заказа |
| Стоимость | Обязательно | quantity * unit_price |
| Модификаторы | Необязательно | JSON: [{group_name, option_name, price}]. Денормализованы |
| Комментарий | Необязательно | Например: “без лука” |
Кухонный статус (kitchen_status) | Обязательно | pending / preparing / ready. Управляется KDS-приложением. Создаётся как pending при переходе заказа в accepted. (Добавлено в BR 5.1) |
Время начала готовки (kitchen_started_at) | Необязательно | Проставляется когда повар тапнул «В работе» на KDS (pending → preparing) |
Время готовности (kitchen_ready_at) | Необязательно | Проставляется когда повар тапнул «Готово» на KDS (preparing → ready) |
Статусная машина
(Обновлено в BR 2.5)
stateDiagram-v2 [*] --> new : Создать заказ new --> accepted : Начать готовку new --> closed : Чекаут (только takeaway без кухни) new --> cancelled : Отменить accepted --> ready : Готово accepted --> cancelled : Отменить (до оплаты) ready --> handed_over : Передать курьеру (delivery) ready --> closed : Оплатить + Закрыть ready --> cancelled : Отменить (до оплаты) handed_over --> in_delivery : Курьер в пути in_delivery --> delivered : Клиент получил delivered --> closed : Закрыть closed --> [*] cancelled --> [*]
Доступные действия по статусу
| Статус | Описание | Доступные действия |
|---|---|---|
new | Заказ создан | Добавить/убрать позиции, «Начать готовку», чекаут (только для takeaway без requires_kitchen), отменить |
accepted | Отправлен в работу (на кухню или в зал) | Отметить готовым, отменить (до оплаты) |
ready | Готов к выдаче / передаче курьеру | Принять оплату (paid_at), закрыть (close), передать курьеру (handed_over), отменить (до оплаты) |
handed_over | Передан курьеру | Курьер начинает путь (start-delivery). Отменить нельзя — только refund |
in_delivery | Курьер в пути | Курьер подтверждает доставку (confirm-delivery). Refund только через возврат заказа |
delivered | Клиент получил | Закрыть (close). Refund только через возврат |
closed | Финальный — оплачен И выдан | Финал. Refund (создание RefundRecord) по закрытому заказу |
cancelled | Отменён | Финал. Отмена возможна только до оплаты |
Нейминг accepted в коде — исторически устоялся, эквивалент юмовского «В процессе». Не переименовываем чтобы не ломать фронт / агрегаторов / live-данные.
Flow по типу заказа
(Добавлено в BR 2.5)
Тип заказа (order_type) + наличие в нём кухонных товаров (requires_kitchen) определяют какой flow применяется.
Быстрый takeaway без кухни (клиент просто взял колу из витрины)
new → closed (одно действие — /checkout)
Используется когда в заказе нет ни одного товара с requires_kitchen=true и order_type=takeaway. Позволяет пропустить accepted/ready и сразу закрыть заказ одним вызовом. Внутри — pay + close атомарно.
Кухонный takeaway / dine_in (шаурма, бургер, суп)
new → (Начать готовку) → accepted → (Готово) → ready → (Оплата) → ready с paid_at → (Закрыть) → closed
Используется когда requires_kitchen=true хотя бы у одной позиции. Кассир явно нажимает «Начать готовку» после приёма заказа — до этого момента кухня не видит тикет (заказ можно ещё менять). После готовности — оплата и выдача разделены.
delivery (доставка курьером)
new → (Начать готовку) → accepted → (Готово) → ready → (Передать курьеру) → handed_over → (Курьер поехал) → in_delivery → (Клиент получил) → delivered → (Оплата) + (Закрыть) → closed
Оплата может происходить либо на точке (при передаче курьеру — префайл), либо у клиента (курьер принимает наличку — тогда pay вызывается после delivered).
Правило расчёта requires_kitchen на заказе
При checkout’е (создании заказа) значение orders.requires_kitchen вычисляется один раз:
orders.requires_kitchen = EXISTS (
order_items JOIN products
WHERE order_id = X AND products.requires_kitchen = true
)
Далее это поле не пересчитывается при изменении заказа (пока статус new можно менять позиции).
KDS-flow позиций
(Добавлено в BR 5.1)
После того как кассир нажимает «Начать готовку» (new → accepted), кухня видит позиции на KDS и управляет их kitchen_status per-item.
Жизненный цикл позиции
pending (по умолчанию при accepted) → preparing (повар тапнул "В работе") → ready (повар тапнул "Готово")
- При переходе заказа в
acceptedвсе его позиции получаютkitchen_status=pending - Позиции с
requires_kitchen=trueпоявляются на KDS своих станций (фильтр поkitchen_station_id) - Повар на KDS меняет:
pending → preparing → ready. Время каждого перехода фиксируется вkitchen_started_at/kitchen_ready_at - Откат
ready → preparingзапрещён. Если повар ошибся — отмена заказа на POS целиком (или per-item refund в P1+)
Кнопка «Готово» по станции на KDS
Закрывает массово все позиции одной станции в одном заказе как ready. Активна когда все позиции этой станции в заказе уже в ready (т.е. это последний жест повара после индивидуальных «Готово»).
Автопереход заказа accepted → ready
Когда все order_items.kitchen_status = ready для всех станций (то есть заказ полностью готов) — Order Service автоматически переводит заказ в ready. Это видит кассир на POS как «можно подавать».
Альтернативный путь к ready (если в заказе нет кухонных позиций) — кассир переводит вручную через действие «Готово» на POS.
Видимость заказа на KDS
Заказ виден на KDS-устройстве со станциями S1..Sn если выполнено всё:
order.store_id = device.store_idorder.status = 'accepted'- В заказе есть хотя бы одна позиция с
requires_kitchen=true AND kitchen_station_id IN (S1..Sn)
Заказ уходит с экрана:
- Когда все позиции выбранных станций перешли в
ready - Или заказ перешёл в
ready/closed/cancelled
При отмене (cancelled) во время готовки — KDS показывает уведомление + звук «Заказ отменён» 2–3 сек, потом убирает карточку.
Paid ≠ Closed
(Добавлено в BR 2.5)
Оплата и закрытие заказа — два разных действия.
Действие «Принять оплату» (pay)
- Проставляет
paid_at,payment_method,paid_amount, опц.rrn/card_last4 - Не меняет статус заказа
- Триггерит событие
order.paid(для Warehouse Service, Customer Service, webhook-подписчиков) - После оплаты заказ может быть в статусе
ready,delivered— любой предзавершающий
Действие «Закрыть» (close)
- Проставляет
completed_at - Статус →
closed - Триггерит событие
order.closed - Инвариант:
closeдопустим только еслиpaid_at IS NOT NULL(исключение: закрытие черезcancel— другой flow)
Сценарии использования
- Кофейня с ожиданием. Клиент оплатил капучино, ждёт у стойки пока бариста сделает. Касса может принимать следующего клиента. Кофе готов → «Закрыть».
- Dine-in в кафе. Клиент доел, попросил счёт, оплатил. Сидит ещё 10 минут. Официант закрывает заказ когда клиент ушёл.
- Доставка с наличной оплатой курьеру. Кухня → ready → передали курьеру → курьер вернулся, передал деньги → касса проводит оплату → закрывает.
Legacy — существующие closed без paid_at
В истории БД есть закрытые заказы без paid_at (до внедрения BR 2.5 — оплата и закрытие склеивались). Такие записи считаются легитимными; инвариант «close требует paid_at» применяется только к новым переходам (после деплоя BR 2.5).
Отмена vs Возврат
(Добавлено в BR 2.5)
Два разных действия с разной семантикой.
Отмена (cancel)
- Допустима пока
paid_at IS NULL - Доступные статусы для отмены:
new,accepted,ready(неоплаченный) - Переводит статус →
cancelled, проставляетcancelled_at,cancel_reason - Не требует работы с деньгами (их не было)
Возврат (refund)
- Применим к оплаченным заказам (
paid_at IS NOT NULL) - Доступные статусы для refund:
closed,delivered, а такжеready/handed_over/in_deliveryесли уже оплачены - Создаёт запись
RefundRecord— детали см. в Data Model Order Service - Возврат идёт тем же способом оплаты (card → card, cash → cash, онлайн-оплата → возврат на карту)
- Полный возврат или частичный (частичный требует указания какие позиции)
Возврат через PayKeeper
(Добавлено в BR 3.3)
Если заказ был оплачен через PayKeeper (orders.pk_payment_id != NULL), возврат инициируется через PK API:
- Сотрудник с
orders.refundнажимает «Вернуть» в карточке закрытого заказа - Выбирает полный/частичный возврат, указывает причину, для частичного — отмечает позиции
- Order Service публикует
order.refund_requested→ Paykeeper Adapter вызываетPOST /change/payment/reverse/в PK - PK принимает запрос async (
{"result":"success"}) - Через N минут PK шлёт refund webhook →
RefundRecord.status=done - При ошибке в PK (
{"result":"fail"}) —RefundRecord.status=failed+ текст ошибки в UI
Для заказов, оплаченных не через PK (legacy или не настроенная интеграция), возврат создаётся локально без внешнего вызова (статус сразу done).
Подробнее: PayKeeper · Flow возврата.
Сводная таблица
| Статус + оплата | Cancel | Refund |
|---|---|---|
new (всегда не оплачен) | ✅ | ❌ |
accepted (не оплачен) | ✅ | ❌ |
ready без paid_at | ✅ | ❌ |
ready с paid_at | ❌ | ✅ |
handed_over / in_delivery | ❌ | ✅ (возврат через возврат заказа на точку) |
delivered | ❌ | ✅ |
closed | ❌ | ✅ |
cancelled | ❌ (финал) | ❌ |
Оплата (операционные правила)
Order Service не проводит платёж — только фиксирует факт. POS-терминал / эквайер проводит оплату, затем вызывает Order Service.
Правила
- Оплата записывается одним вызовом: способ, сумма, RRN (для карты), последние 4 цифры
- Фиксация оплаты не меняет статус (BR 2.5 — изменено; раньше меняла на
ready) - Повторная оплата запрещена (ошибка
ORDER_ALREADY_PAID) - Множественные способы оплаты —
mixed(Phase 2: детализация)
Ролевая матрица
(Обновлено в BR 2.5 — добавлены курьерские переходы и orders.delivery)
| Действие | Permission | Франшиза | Франчайзи | Менеджер | Кассир | Курьер |
|---|---|---|---|---|---|---|
| Просмотр заказов (админка) | orders.read | Все ТТ | Свои ТТ | Своя ТТ | Нет | Нет |
| Создать заказ (API) | orders.edit | — | — | — | Да | — |
Редактировать позиции (до accepted) | orders.edit | — | — | — | Да | — |
| «Начать готовку» (new → accepted) | orders.edit | — | — | — | Да | — |
| «Готово» (accepted → ready) | orders.edit | — | — | — | Да | — |
Принять оплату (pay) | orders.edit | — | — | — | Да | — |
Закрыть заказ (close) | orders.edit | — | — | — | Да | — |
| Отменить (только до оплаты) | orders.edit | Любой | Свои ТТ | Своя ТТ | Свой | — |
| Refund по закрытому | orders.edit + специальная логика | ✅ | ✅ | По настройкам | По настройкам | — |
Передать курьеру (hand-over-to-courier) | orders.edit | — | — | — | Да | — |
Курьер начал путь (start-delivery) | orders.delivery | — | — | — | — | Да |
Курьер подтвердил доставку (confirm-delivery) | orders.delivery | — | — | — | — | Да |
Permission orders.delivery — отдельная, выдаётся курьерам. Не включает orders.edit — курьер не может менять состав заказа или делать возвраты.
Бизнес-правила
- Номер per-store per-day — #001 сбрасывается каждый день для каждой ТТ
- Позиции только в
new— добавлять/убирать можно пока статусnew - Отмена — допустима из
new,in_progress,ready. Изclosedнельзя (возврат — Phase 2) - Оплата — фиксация факта, не проведение. POS отвечает за платёж
- Денормализация — product_name и unit_price копируются в позицию заказа на момент создания (не зависят от будущих изменений каталога)
- Модификаторы — JSON в order_item, денормализованы (имя группы, имя опции, цена на момент заказа)
- Пересчёт total — итого заказа пересчитывается при каждом добавлении/удалении позиции
Что НЕ входит (Phase 2+)
(Обновлено в BR 2.5 — вычеркнуто то что вошло)
Реализация статуса— ✅ реализовано в BR 2.5 (статусin_progress(кухня)accepted)Доставка (курьеры)— ✅ базовый flowhanded_over → in_delivery → deliveredреализован в BR 2.5Возвраты/рефанды (базовые)— ✅RefundRecordреализован (Week 3 POS pre-prod)KDS как отдельное приложение (экран для кухни с item-level статусами)— ✅ в работе через BR 5.1Item-level статусы позиций (каждая позиция готова/в процессе отдельно)— ✅ в работе через BR 5.1 (order_items.kitchen_status)- Split-payment по стульям в dine-in — Phase 2
- Автозакрытие заказа по таймеру (
paid → closedчерез N секунд автоматически) — Phase 2 - Физические кухонные принтеры (ESC/POS, печать тикета на принтере конкретной станции) — BR 5.4
- Скидки/промокоды — Phase 3 (Loyalty)
- Лояльность (баллы как оплата) — Phase 3
- Авто-списание со склада при продаже (Kafka → Warehouse) — есть базовая реализация; пересмотр семантики «когда списывать: на
paidилиclosed» — открытый вопрос BR 2.5 - POS-терминал UI (собственный) — отказались, используем PayKeeper + внешние POS-фронты через API + webhook-подписки
- Авторизация внешних подписчиков (OAuth вместо HMAC) — Phase 2
- Admin-UI управления webhook-подписками — Phase 2 (в MVP — через SQL/internal endpoint, см. Webhook-подписки)
Привязка клиента к заказу — вошла в scope
Ссылки
- BR 2.1 — базовая модель
- BR 3.1 — привязка клиента к заказу
- BR 2.5 — расширенная статусная модель, Paid≠Closed, flow по типу, delivery substatus
- BR 5.1 —
kitchen_statusper-item,expected_ready_at, KDS-flow - BR 2.3 — research-заметка, Yuma-паритет полностью (item-level и т.п.)
- Order Service — Overview
- Order Service — API
- Order Service — Data Model
- Order Service — Events
- Клиенты — сущность клиента, POS-flow прикрепления
- Каталог — поля
requires_kitchen,kitchen_station_id - Кухонные станции — сущность станции + цветовые пороги для KDS
- KDS — Кухонный экран — потребитель
kitchen_statusиexpected_ready_at - Webhook-подписки — механизм уведомлений внешних POS (KOALa)
- Агрегаторы доставки — соседний тип интеграции
- Ролевая модель