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.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы (из JWT)
store_iduuidNOT NULLТорговая точка ( Store Service, кросс-сервисная ссылка)
customer_iduuidNULLКлиент, привязанный к заказу. Кросс-сервисная ссылка Customer Service. Заказ может быть анонимным (NULL). (Добавлено в BR 3.1)
order_numbervarchar(20)NOT NULLНомер заказа per-store per-day: “001”, “002”…
order_typevarchar(20)NOT NULL'takeaway'Тип: takeaway / dine_in / delivery
statusvarchar(20)NOT NULL'new'Статус: new / in_progress / accepted / ready / handed_over / in_delivery / delivered / closed / cancelled / rejected. (Расширено в BR 2.5 — добавлены in_delivery, delivered)
requires_kitchenbooleanNOT NULLfalseРассчитывается на checkout из позиций (EXISTS order_items JOIN products WHERE requires_kitchen=true). Определяет flow. (Добавлено в BR 2.5)
channelvarchar(32)NOT NULL'INTERNAL'Канал происхождения: INTERNAL / YANDEX_EDA / MARKET_DELIVERY / внешние POS каналы
totaldecimal(12,2)NOT NULL0Сумма всех позиций
payment_methodvarchar(20)NULLСпособ оплаты: cash / card / qr / mixed
paid_amountdecimal(12,2)NULLФактическая сумма оплаты
rrnvarchar(50)NULLRRN транзакции (для карточных платежей)
card_last4varchar(4)NULLПоследние 4 цифры карты
pk_invoice_idvarchar(50)NULLID инвойса PayKeeper, заполняется после paykeeper.invoice.created. (BR 3.3)
pk_invoice_urltextNULLURL для оплаты (отдаётся клиенту / терминалу PK). (BR 3.3)
pk_payment_idvarchar(50)NULLID платежа в PayKeeper после paykeeper.payment.received. Используется для возвратов. (BR 3.3)
pk_fop_receipt_keyvarchar(100)NULLКлюч фискального чека (для URL страницы чека у PK). (BR 3.3)
fiscal_datajsonbNULL{fpd, fnd, fn, rnkkt, shift_number, receipt_number, ts} — заполняется по paykeeper.receipt.fiscalized. (BR 3.3)
fiscal_failedbooleanNOT NULLfalsetrue если PK вернул rejected/failed/timeout при формировании чека. Визуальная метка в UI. (BR 3.3)
commenttextNULLКомментарий к заказу
cancel_reasontextNULLПричина отмены
created_byuuidNOT NULLКто создал (user_id из JWT)
accepted_byuuidNULLКто нажал «Начать готовку» (user_id). (BR 2.5)
courier_iduuidNULLКурьер на доставке (user_id, для ролевой проверки orders.delivery). (BR 2.5)
accepted_attimestampNULLМомент перехода new → accepted. (BR 2.5)
ready_attimestampNULLМомент перехода accepted → ready. (BR 2.5)
expected_ready_attimestampNULLПлановая готовность. Проставляется при переходе new → accepted: accepted_at + avg_prep_time(station) (или +15 мин если данных о ТТ нет). Используется KDS для расчёта цвета карточки и времени просрочки. (Добавлено в BR 5.1)
paid_attimestampNULLДата фиксации оплаты
handed_over_attimestampNULLПередан курьеру. (BR 2.5)
in_delivery_attimestampNULLКурьер начал путь. (BR 2.5)
delivered_attimestampNULLКурьер передал клиенту. (BR 2.5)
completed_attimestampNULLДата закрытия (closed). Инвариант: закрытие требует paid_at IS NOT NULL (кроме legacy-записей)
cancelled_attimestampNULLДата отмены
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • 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 / delivered BR 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, модификаторы копируются при создании.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
order_iduuidNOT NULLFK orders.id
product_iduuidNOT NULLТовар ( Catalog Service, кросс-сервисная ссылка)
product_namevarchar(255)NOT NULLНазвание товара (денормализовано)
quantitydecimal(10,3)NOT NULLКоличество
unit_pricedecimal(10,2)NOT NULLЦена за единицу (денормализовано)
total_pricedecimal(12,2)NOT NULLСтоимость: quantity * unit_price
modifiersjsonbNULLМодификаторы: [{group_name, option_name, price}]
notestextNULLКомментарий к позиции (“без лука”)
kitchen_statusvarchar(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_attimestampNULLКогда повар тапнул «В работе» (pending → preparing). (BR 5.1)
kitchen_ready_attimestampNULLКогда повар тапнул «Готово» (preparing → ready). (BR 5.1)
created_attimestampNOT NULLnow()

Ограничения:

  • FK order_id REFERENCES orders(id) ON DELETE CASCADE
  • CHECK (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.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
order_iduuidNOT NULLFK orders.id
client_refund_idvarchar(100)NOT NULLИдемпотентный ключ (= externalId чека возврата в ФЯ)
amountdecimal(12,2)NOT NULLСумма возврата
typevarchar(20)NOT NULL'refund'refund или reversal (банк решает реально)
payment_methodvarchar(20)NOT NULLcash или card
rrnvarchar(50)NULLRRN возврата (для card)
fiscal_doc_numbervarchar(50)NULLНомер фискального документа чека возврата
initiated_byvarchar(10)NOT NULL'pos'pos или admin
cashier_iduuidNOT NULLКто провёл возврат
created_attimestampNOT NULLnow()

Ограничения:

  • 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_idStore Service stores.idТорговая точка
orders.created_byUser Service users.idКто создал заказ
order_items.product_idCatalog Service products.idТовар

Денормализация

product_name, unit_price и modifiers копируются в order_items при создании позиции. Это гарантирует неизменность данных заказа даже при обновлении каталога.


KDS-flow: автоматические переходы

(Добавлено в BR 5.1)

При переходе orders.status = new → accepted (вызов POST /orders/{id}/start-cooking):

  1. Проставляется accepted_at = NOW(), kitchen_started_at = NOW() (если ещё не было)
  2. Все позиции этого заказа получают kitchen_status = 'pending' (default из DDL)
  3. Рассчитывается expected_ready_at = accepted_at + avg_prep_time(station) (или +15 мин)
  4. Публикуется событие order.cooking_started (см. Events)

При обновлении kitchen_status любой позиции через KDS-endpoints:

  1. Если preparingkitchen_started_at = NOW() (только в order_items, на orders уже зафиксирован)
  2. Если readykitchen_ready_at = NOW()
  3. Публикуется событие order.item.kitchen_status_changed
  4. 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).