Order Service — Data Model
База данных: order_db
(Добавлено в BR 2.1)
erDiagram orders { uuid id PK uuid franchise_id "NOT NULL" uuid store_id "NOT NULL (-> Store Service)" uuid customer_id "NULL (-> Customer Service, BR 3.1)" varchar order_number "NOT NULL, (20)" varchar order_type "NOT NULL, default takeaway: dine_in / takeaway / delivery" varchar status "NOT NULL, default new" boolean requires_kitchen "NOT NULL, default false (BR 2.5)" varchar channel "NOT NULL, default INTERNAL: INTERNAL / aggregator" varchar external_provider "NULL: yandex_eda / delivery_club" varchar external_order_id "NULL, (100)" boolean cancelled_by_customer "NOT NULL, default false" uuid table_id "NULL (-> Store Service zal_tables)" timestamp kitchen_started_at "NULL" uuid waiter_id "NULL — BR 3.2, официант заказа" decimal tip_amount "NULL, (10,2) — BR 3.2, сумма чаевых (обогащается из tips.received)" decimal total "NOT NULL, default 0, (12,2)" varchar payment_method "NULL, (20)" decimal paid_amount "NULL, (12,2)" varchar rrn "NULL, (50)" varchar card_last4 "NULL, (4)" varchar pk_invoice_id "NULL, (50) — BR 3.3, PayKeeper invoice" text pk_invoice_url "NULL — BR 3.3, URL для оплаты" varchar pk_payment_id "NULL, (50) — BR 3.3, PayKeeper payment ID после оплаты" varchar pk_fop_receipt_key "NULL, (100) — BR 3.3, ключ фискального чека" jsonb fiscal_data "NULL — BR 3.3, {fpd, fnd, fn, rnkkt, shift_number, receipt_number}" boolean fiscal_failed "NOT NULL, default false — BR 3.3, метка провала фискализации" text comment "NULL" text cancel_reason "NULL" uuid created_by "NOT NULL" uuid accepted_by "NULL (BR 2.5)" uuid courier_id "NULL (BR 2.5)" timestamp accepted_at "NULL (BR 2.5)" timestamp ready_at "NULL (BR 2.5)" timestamp expected_ready_at "NULL (BR 5.1) — плановая готовность для KDS" timestamp paid_at "NULL" timestamp handed_over_at "NULL (BR 2.5)" timestamp in_delivery_at "NULL (BR 2.5)" timestamp delivered_at "NULL (BR 2.5)" timestamp completed_at "NULL" timestamp cancelled_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } order_items { uuid id PK uuid order_id FK "NOT NULL -> orders.id CASCADE" uuid product_id "NOT NULL (-> Catalog Service)" varchar product_name "NOT NULL, (255)" decimal quantity "NOT NULL, (10,3)" decimal unit_price "NOT NULL, (10,2)" decimal total_price "NOT NULL, (12,2)" int assembly_time_seconds "NULL — snapshot из Catalog для ETA kitchen" jsonb modifiers "NULL" text notes "NULL" varchar kitchen_status "NOT NULL, default pending (BR 5.1): pending / preparing / ready" timestamp kitchen_started_at "NULL (BR 5.1) — повар тапнул В работе" timestamp kitchen_ready_at "NULL (BR 5.1) — повар тапнул Готово" timestamp created_at "NOT NULL" } refund_records { uuid id PK uuid order_id FK "NOT NULL -> orders.id" varchar client_refund_id "NOT NULL UNIQUE, (100)" decimal amount "NOT NULL, > 0, (12,2)" varchar type "NOT NULL, default refund, (20)" varchar payment_method "NOT NULL, (20)" varchar rrn "NULL, (50)" varchar fiscal_doc_number "NULL, (50)" varchar initiated_by "NOT NULL, default pos, (10)" uuid cashier_id "NOT NULL" timestamp created_at "NOT NULL" } refund_requests { uuid id PK uuid order_id FK "NOT NULL -> orders.id" decimal amount "NOT NULL, (12,2)" text reason "NOT NULL" varchar status "NOT NULL, default pending: pending / confirmed / rejected" uuid requested_by_admin_id "NOT NULL" uuid confirmed_by_cashier_id "NULL" uuid refund_record_id "NULL (set after POS confirms and refund_records is created)" timestamp created_at "NOT NULL" timestamp confirmed_at "NULL" timestamp rejected_at "NULL" text reject_reason "NULL" } orders ||--o{ order_items : "contains" orders ||--o{ refund_records : "refunded by" orders ||--o{ refund_requests : "has pending refund requests"
Таблицы
orders
(BR 2.1)
Заказы. Привязаны к торговой точке. Нумерация per-store per-day.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы (из JWT) |
store_id | uuid | NOT NULL | — | Торговая точка (→ Store Service, кросс-сервисная ссылка) |
customer_id | uuid | NULL | — | Клиент, привязанный к заказу. Кросс-сервисная ссылка → Customer Service. Заказ может быть анонимным (NULL). (Добавлено в BR 3.1) |
order_number | varchar(20) | NOT NULL | — | Номер заказа per-store per-day: “001”, “002”… |
order_type | varchar(20) | NOT NULL | 'takeaway' | Тип: takeaway / dine_in / delivery |
status | varchar(20) | NOT NULL | 'new' | Статус: new / in_progress / accepted / ready / handed_over / in_delivery / delivered / closed / cancelled / rejected. (Расширено в BR 2.5 — добавлены in_delivery, delivered) |
requires_kitchen | boolean | NOT NULL | false | Рассчитывается на checkout из позиций (EXISTS order_items JOIN products WHERE requires_kitchen=true). Определяет flow. (Добавлено в BR 2.5) |
channel | varchar(32) | NOT NULL | 'INTERNAL' | Канал происхождения: INTERNAL / YANDEX_EDA / MARKET_DELIVERY / внешние POS каналы |
total | decimal(12,2) | NOT NULL | 0 | Сумма всех позиций |
payment_method | varchar(20) | NULL | — | Способ оплаты: cash / card / qr / mixed |
paid_amount | decimal(12,2) | NULL | — | Фактическая сумма оплаты |
rrn | varchar(50) | NULL | — | RRN транзакции (для карточных платежей) |
card_last4 | varchar(4) | NULL | — | Последние 4 цифры карты |
pk_invoice_id | varchar(50) | NULL | — | ID инвойса PayKeeper, заполняется после paykeeper.invoice.created. (BR 3.3) |
pk_invoice_url | text | NULL | — | URL для оплаты (отдаётся клиенту / терминалу PK). (BR 3.3) |
pk_payment_id | varchar(50) | NULL | — | ID платежа в PayKeeper после paykeeper.payment.received. Используется для возвратов. (BR 3.3) |
pk_fop_receipt_key | varchar(100) | NULL | — | Ключ фискального чека (для URL страницы чека у PK). (BR 3.3) |
fiscal_data | jsonb | NULL | — | {fpd, fnd, fn, rnkkt, shift_number, receipt_number, ts} — заполняется по paykeeper.receipt.fiscalized. (BR 3.3) |
fiscal_failed | boolean | NOT NULL | false | true если PK вернул rejected/failed/timeout при формировании чека. Визуальная метка в UI. (BR 3.3) |
comment | text | NULL | — | Комментарий к заказу |
cancel_reason | text | NULL | — | Причина отмены |
created_by | uuid | NOT NULL | — | Кто создал (user_id из JWT) |
accepted_by | uuid | NULL | — | Кто нажал «Начать готовку» (user_id). (BR 2.5) |
courier_id | uuid | NULL | — | Курьер на доставке (user_id, для ролевой проверки orders.delivery). (BR 2.5) |
accepted_at | timestamp | NULL | — | Момент перехода new → accepted. (BR 2.5) |
ready_at | timestamp | NULL | — | Момент перехода accepted → ready. (BR 2.5) |
expected_ready_at | timestamp | NULL | — | Плановая готовность. Проставляется при переходе new → accepted: accepted_at + avg_prep_time(station) (или +15 мин если данных о ТТ нет). Используется KDS для расчёта цвета карточки и времени просрочки. (Добавлено в BR 5.1) |
paid_at | timestamp | NULL | — | Дата фиксации оплаты |
handed_over_at | timestamp | NULL | — | Передан курьеру. (BR 2.5) |
in_delivery_at | timestamp | NULL | — | Курьер начал путь. (BR 2.5) |
delivered_at | timestamp | NULL | — | Курьер передал клиенту. (BR 2.5) |
completed_at | timestamp | NULL | — | Дата закрытия (closed). Инвариант: закрытие требует paid_at IS NOT NULL (кроме legacy-записей) |
cancelled_at | timestamp | NULL | — | Дата отмены |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
CHECK (order_type IN ('takeaway', 'dine_in', 'delivery'))CHECK (status IN ('new', 'in_progress', 'accepted', 'ready', 'handed_over', 'in_delivery', 'delivered', 'closed', 'cancelled', 'rejected'))(миграция 005 — агрегаторский flow; миграция 008 —in_delivery/deliveredBR 2.5)- Кросс-сервисные ссылки:
store_id→ Store Service,created_by/accepted_by/courier_id→ User Service (не FK, lookup по API)
Инвариант (проверяется в сервисе, не в БД):
status = 'closed'⇒paid_at IS NOT NULL(для новых записей после BR 2.5; legacy-записи безpaid_atдопустимы)
Индексы:
idx_orders_franchise_store_status—(franchise_id, store_id, status)idx_orders_store_created_at—(store_id, created_at)idx_orders_customer—(customer_id) WHERE customer_id IS NOT NULL— для lookup истории заказов клиента (BR 3.1)
order_items
(BR 2.1)
Позиции заказа. Денормализованы: product_name, unit_price, модификаторы копируются при создании.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
order_id | uuid | NOT NULL | — | FK → orders.id |
product_id | uuid | NOT NULL | — | Товар (→ Catalog Service, кросс-сервисная ссылка) |
product_name | varchar(255) | NOT NULL | — | Название товара (денормализовано) |
quantity | decimal(10,3) | NOT NULL | — | Количество |
unit_price | decimal(10,2) | NOT NULL | — | Цена за единицу (денормализовано) |
total_price | decimal(12,2) | NOT NULL | — | Стоимость: quantity * unit_price |
modifiers | jsonb | NULL | — | Модификаторы: [{group_name, option_name, price}] |
notes | text | NULL | — | Комментарий к позиции (“без лука”) |
kitchen_status | varchar(20) | NOT NULL | 'pending' | KDS-статус позиции: pending / preparing / ready. Создаётся как pending при переходе заказа в accepted. Управляется KDS-приложением через PATCH /orders/{id}/items/{itemId}/kitchen-status. Откат ready → preparing запрещён. (Добавлено в BR 5.1) |
kitchen_started_at | timestamp | NULL | — | Когда повар тапнул «В работе» (pending → preparing). (BR 5.1) |
kitchen_ready_at | timestamp | NULL | — | Когда повар тапнул «Готово» (preparing → ready). (BR 5.1) |
created_at | timestamp | NOT NULL | now() |
Ограничения:
FK order_id REFERENCES orders(id) ON DELETE CASCADECHECK (kitchen_status IN ('pending', 'preparing', 'ready'))(BR 5.1)- Кросс-сервисные ссылки:
product_id→ Catalog Service (не FK)
Индексы:
idx_order_items_order_id—(order_id)idx_order_items_kitchen_status—(order_id, kitchen_status)— для быстрой проверки «все ли позиции готовы» при автопереходеaccepted → ready(BR 5.1)
refund_records
(Добавлено в Week 3 — refund flow, POS pre-prod)
Записи возвратов по заказам. Одна продажа может иметь несколько refund-записей (частичные возвраты). client_refund_id гарантирует идемпотентность при повторном POS→BFF retry.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
order_id | uuid | NOT NULL | — | FK → orders.id |
client_refund_id | varchar(100) | NOT NULL | — | Идемпотентный ключ (= externalId чека возврата в ФЯ) |
amount | decimal(12,2) | NOT NULL | — | Сумма возврата |
type | varchar(20) | NOT NULL | 'refund' | refund или reversal (банк решает реально) |
payment_method | varchar(20) | NOT NULL | — | cash или card |
rrn | varchar(50) | NULL | — | RRN возврата (для card) |
fiscal_doc_number | varchar(50) | NULL | — | Номер фискального документа чека возврата |
initiated_by | varchar(10) | NOT NULL | 'pos' | pos или admin |
cashier_id | uuid | NOT NULL | — | Кто провёл возврат |
created_at | timestamp | NOT NULL | now() |
Ограничения:
UNIQUE (client_refund_id)— идемпотентностьCHECK (type IN ('refund', 'reversal'))CHECK (initiated_by IN ('pos', 'admin'))CHECK (amount > 0)FK order_id REFERENCES orders(id)- Инвариант (проверяется в сервисе):
SUM(amount) WHERE order_id = X ≤ orders.total
Индексы:
idx_refund_records_order_id—(order_id)idx_refund_records_created_at—(created_at)uq_refund_records_client_id(UNIQUE) —(client_refund_id)
Кросс-сервисные ссылки
Order Service хранит UUID из других сервисов. Это не FK в БД:
| Поле | Источник | Описание |
|---|---|---|
orders.store_id | Store Service → stores.id | Торговая точка |
orders.created_by | User Service → users.id | Кто создал заказ |
order_items.product_id | Catalog Service → products.id | Товар |
Денормализация
product_name,unit_priceиmodifiersкопируются вorder_itemsпри создании позиции. Это гарантирует неизменность данных заказа даже при обновлении каталога.
KDS-flow: автоматические переходы
(Добавлено в BR 5.1)
При переходе orders.status = new → accepted (вызов POST /orders/{id}/start-cooking):
- Проставляется
accepted_at = NOW(),kitchen_started_at = NOW()(если ещё не было) - Все позиции этого заказа получают
kitchen_status = 'pending'(default из DDL) - Рассчитывается
expected_ready_at = accepted_at + avg_prep_time(station)(или+15 мин) - Публикуется событие
order.cooking_started(см. Events)
При обновлении kitchen_status любой позиции через KDS-endpoints:
- Если
preparing—kitchen_started_at = NOW()(только вorder_items, на orders уже зафиксирован) - Если
ready—kitchen_ready_at = NOW() - Публикуется событие
order.item.kitchen_status_changed - Auto-transition check: если все
order_items.kitchen_status = 'ready'для всех позиций заказа →orders.status = 'ready',ready_at = NOW(), событиеorder.ready
Миграция: XX-add-kds-fields.xml (Liquibase changeset) — добавление колонок:
orders.expected_ready_at(NULL)order_items.kitchen_status(varchar(20), NOT NULL DEFAULT ‘pending’)order_items.kitchen_started_at(timestamp, NULL)order_items.kitchen_ready_at(timestamp, NULL)- Индекс
idx_order_items_kitchen_statusна(order_id, kitchen_status)
Backfill для legacy-записей: kitchen_status='ready' для всех order_items где соответствующий orders.status IN ('ready','closed','delivered') (чтобы старые завершённые заказы не появлялись на KDS).