Заказы

Источники требований

  • BR 2.1 — Order Service — базовая модель
  • BR 3.1 — привязка клиента к заказу
  • BR 2.5 — расширенная статусная модель, Paid≠Closed, flow по типу заказа, delivery substatus, кухонные станции, webhook-подписки
  • BR 5.1kitchen_status per-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_id
  • order.status = 'accepted'
  • В заказе есть хотя бы одна позиция с requires_kitchen=true AND kitchen_station_id IN (S1..Sn)

Заказ уходит с экрана:

  • Когда все позиции выбранных станций перешли в ready
  • Или заказ перешёл в ready / closed / cancelled

При отмене (cancelled) во время готовки — KDS показывает уведомление + звук «Заказ отменён» 2–3 сек, потом убирает карточку.


(Добавлено в 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)

Сценарии использования

  1. Кофейня с ожиданием. Клиент оплатил капучино, ждёт у стойки пока бариста сделает. Касса может принимать следующего клиента. Кофе готов → «Закрыть».
  2. Dine-in в кафе. Клиент доел, попросил счёт, оплатил. Сидит ещё 10 минут. Официант закрывает заказ когда клиент ушёл.
  3. Доставка с наличной оплатой курьеру. Кухня → 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:

  1. Сотрудник с orders.refund нажимает «Вернуть» в карточке закрытого заказа
  2. Выбирает полный/частичный возврат, указывает причину, для частичного — отмечает позиции
  3. Order Service публикует order.refund_requested → Paykeeper Adapter вызывает POST /change/payment/reverse/ в PK
  4. PK принимает запрос async ({"result":"success"})
  5. Через N минут PK шлёт refund webhook → RefundRecord.status=done
  6. При ошибке в PK ({"result":"fail"}) — RefundRecord.status=failed + текст ошибки в UI

Для заказов, оплаченных не через PK (legacy или не настроенная интеграция), возврат создаётся локально без внешнего вызова (статус сразу done).

Подробнее: PayKeeper · Flow возврата.

Сводная таблица

Статус + оплатаCancelRefund
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 — курьер не может менять состав заказа или делать возвраты.


Бизнес-правила

  1. Номер per-store per-day — #001 сбрасывается каждый день для каждой ТТ
  2. Позиции только в new — добавлять/убирать можно пока статус new
  3. Отмена — допустима из new, in_progress, ready. Из closed нельзя (возврат — Phase 2)
  4. Оплата — фиксация факта, не проведение. POS отвечает за платёж
  5. Денормализация — product_name и unit_price копируются в позицию заказа на момент создания (не зависят от будущих изменений каталога)
  6. Модификаторы — JSON в order_item, денормализованы (имя группы, имя опции, цена на момент заказа)
  7. Пересчёт total — итого заказа пересчитывается при каждом добавлении/удалении позиции

Что НЕ входит (Phase 2+)

(Обновлено в BR 2.5 — вычеркнуто то что вошло)

  • Реализация статуса in_progress (кухня) — ✅ реализовано в BR 2.5 (статус accepted)
  • Доставка (курьеры) — ✅ базовый flow handed_over → in_delivery → delivered реализован в BR 2.5
  • Возвраты/рефанды (базовые) — ✅ RefundRecord реализован (Week 3 POS pre-prod)
  • KDS как отдельное приложение (экран для кухни с item-level статусами) — ✅ в работе через BR 5.1
  • Item-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

Перенесена в MVP в BR 3.1. Поле Клиент в сущности заказа + POS-flow прикрепления — описаны в Клиенты.


Ссылки