Aggregator Service — Data Model

ER-диаграмма

erDiagram
    bindings ||--o{ aggregator_logs : "logs per binding"
    bindings ||--o{ menu_snapshots : "menu cache"
    bindings {
      uuid id PK
      uuid store_id FK
      uuid franchise_id
      string provider
      string external_restaurant_id
      string client_id_encrypted
      string client_secret_encrypted
      decimal commission_percent
      boolean is_enabled
      timestamp connected_at
      timestamp last_menu_sync_at
      timestamp last_order_received_at
      timestamp deleted_at
      timestamp created_at
      timestamp updated_at
    }
    aggregator_logs {
      uuid id PK
      uuid binding_id FK
      string direction
      string operation
      string external_reference
      int status_code
      text request_body
      text response_body
      int duration_ms
      timestamp created_at
    }
    menu_snapshots {
      uuid id PK
      uuid binding_id FK
      text snapshot_json
      string snapshot_hash
      timestamp built_at
      timestamp expires_at
    }
    incoming_orders {
      uuid id PK
      uuid binding_id FK
      string external_order_id
      string status
      uuid order_id
      text payload_json
      timestamp received_at
    }
    status_push_queue {
      uuid id PK
      uuid order_id
      uuid binding_id FK
      string target_status
      string reason
      int attempts
      timestamp next_attempt_at
      timestamp done_at
      text last_error
    }

    webhook_subscriptions ||--o{ webhook_delivery_attempts : "log"
    webhook_subscriptions {
      uuid id PK
      uuid franchise_id "NOT NULL"
      uuid store_id "NULL = all stores of franchise"
      varchar subscriber_name "NOT NULL, (100)"
      text webhook_url "NOT NULL"
      text secret_encrypted "NOT NULL"
      jsonb events "NOT NULL — array of event types"
      boolean is_active "NOT NULL, default true"
      timestamp deleted_at "NULL"
      timestamp created_at "NOT NULL"
      timestamp updated_at "NOT NULL"
    }
    webhook_delivery_attempts {
      uuid id PK
      uuid subscription_id FK "NOT NULL"
      uuid event_id "NOT NULL — from Kafka event"
      varchar event_type "NOT NULL, (100)"
      varchar payload_hash "NOT NULL, (64)"
      int attempt_number "NOT NULL"
      int http_status "NULL"
      text error "NULL"
      timestamp next_attempt_at "NULL"
      timestamp delivered_at "NULL"
      timestamp created_at "NOT NULL"
    }
    tip_events {
      uuid id PK
      uuid binding_id FK
      string provider "netmonet"
      string external_tip_id "UNIQUE with binding_id"
      string order_number "NULL"
      int table_number "NULL"
      string waiter_netmonet_id "NULL"
      uuid waiter_id "NULL — резолв через employees.netmonet_profile_id"
      decimal amount
      string currency "RUB"
      timestamp received_at
      text raw_json
      timestamp processed_at "NULL"
      timestamp created_at
    }

Таблицы

bindings — подключение ТТ к агрегатору

ПолеТипОписание
idUUIDPK
store_idUUIDссылка на нашу ТТ (FK логический — Store Service в другой БД)
franchise_idUUIDдля мультитенантности
providervarchar(32)yandex-eda, market-delivery, …
external_restaurant_idvarchar(100)ID ресторана в системе провайдера
client_id_encryptedtextOAuth client_id (AES encrypted at rest)
client_secret_encryptedtextOAuth client_secret
commission_percentdecimal(5,2)например 25.00
is_enabledbooleantrue = принимаем заказы
connected_attimestampкогда первый раз подключили
last_menu_sync_attimestampпоследняя успешная отдача снапшота
last_order_received_attimestampпоследний принятый заказ
deleted_attimestampsoft delete
created_at / updated_attimestamp

Индексы:

  • unique(store_id, provider) where deleted_at is null — одна ТТ → одно подключение к каждому провайдеру
  • idx(franchise_id, provider)

aggregator_logs — лог всех pull/push

ПолеТипОписание
idUUIDPK
binding_idUUIDFK
directionvarchar(10)inbound / outbound
operationvarchar(50)menu_sync, stoplist_pull, order_new, order_cancel, status_push, …
external_referencevarchar(100)external_order_id если применимо
status_codeintHTTP status
request_bodytextтело запроса (при outbound — что мы послали; при inbound — что получили)
response_bodytextответ
duration_msintвремя запроса
created_attimestamp

Retention: 30 дней (cron на удаление).

Чтобы не пересобирать JSON меню на каждый pull — кэшируем с TTL.

ПолеТипОписание
idUUIDPK
binding_idUUIDFK
snapshot_jsontextготовый ответ для агрегатора
snapshot_hashvarchar(64)SHA256 для дедупа
built_attimestampкогда пересобрали
expires_attimestampbuilt_at + 1 час

При событии catalog.updated / product.availability.changed — инвалидация (ставим expires_at = now).

incoming_orders — реестр принятых заказов

Для идемпотентности и отладки.

ПолеТипОписание
idUUIDPK
binding_idUUIDFK
external_order_idvarchar(100)ID в системе провайдера
statusvarchar(20)accepted, rejected, duplicate
order_idUUIDнаш Order Service order_id (nullable до создания)
payload_jsontextraw payload от провайдера
received_attimestamp

unique(binding_id, external_order_id) — дедуп.

status_push_queue — очередь исходящих push-обновлений

Если push упал, retry.

ПолеТипОписание
idUUIDPK
order_idUUIDнаш order
binding_idUUIDFK
target_statusvarchar(20)accepted, cooking, ready, handed_over, rejected, cancelled
reasonvarchar(50)для rejected
attemptsint0..max
next_attempt_attimestampкогда следующая попытка (exponential backoff)
done_attimestampуспешно отправлено
last_errortext

Worker с cron раз в минуту перебирает done_at is null and next_attempt_at <= now и push-ит.

webhook_subscriptions — подписки внешних клиентов (не-агрегаторов)

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

Подписки внешних POS-систем (KOALa, KDS-фронты, аналитические сервисы) на события Order Service. Бизнес-спека: Webhook-подписки.

ПолеТипОписание
idUUIDPK
franchise_idUUIDмультитенантность
store_idUUIDnullable; NULL = подписка на все ТТ франшизы
subscriber_namevarchar(100)koala, kitchen-display, analytics — человекочитаемое имя
webhook_urltextHTTPS URL куда POST-ить события
secret_encryptedtextсекрет для HMAC-SHA256 подписи тела (AES-encrypted at rest)
eventsjsonbмассив типов событий: ["order.paid", "order.closed", ...]
is_activebooleandefault true
deleted_attimestampsoft delete
created_at / updated_attimestamp

Индексы:

  • unique(franchise_id, store_id, webhook_url) WHERE deleted_at IS NULL AND is_active = true
  • idx(franchise_id) WHERE is_active = true

webhook_delivery_attempts — лог попыток доставки

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

Аудит и retry-логика для webhook-доставки.

ПолеТипОписание
idUUIDPK
subscription_idUUID FK→ webhook_subscriptions
event_idUUIDID события из Kafka (для идемпотентности)
event_typevarchar(100)order.paid, etc.
payload_hashvarchar(64)SHA256 тела запроса
attempt_numberint1..N
http_statusintHTTP-ответ подписчика (NULL если сетевая ошибка)
errortextтекст ошибки
next_attempt_attimestampкогда повторить; NULL если успех или dead-letter
delivered_attimestampкогда успешно доставлено (2xx); NULL до первого успеха
created_attimestamp

Retry-политика (backoff): 10s → 30s → 2m → 10m → 1h → 6h → 24h → dead-letter (после 7 неуспехов).

Retention: 30 дней — cron чистит старше.

Worker:

  • Один worker слушает Kafka-топики Order Service (order.*)
  • Находит подходящие подписки для каждого события (фильтрация по franchise_id, store_id, events)
  • Создаёт запись webhook_delivery_attempt и пытается доставить
  • При неудаче обновляет next_attempt_at с backoff

Связанные таблицы в других сервисах

Catalog Service — product_external_mappings (новая)

ПолеТип
idUUID PK
product_idUUID FK → products
providervarchar(32)
external_skuvarchar(100)
price_overridedecimal(10,2) nullable
is_availableboolean default true

unique(product_id, provider).

Catalog Service — products (добавляется поле)

ПолеТип
available_in_aggregatorsboolean default false — глобально, показывать ли в агрегаторах

Store Service — stores.supports_aggregators (добавляется)

Для управления “ТТ может принимать заказы агрегаторов” на уровне ТТ.

Order Service — orders (добавляются поля)

ПолеТипОписание
channelvarchar(32) default INTERNALINTERNAL, YANDEX_EDA, …
external_order_idvarchar(100) nullableID в системе агрегатора
external_providervarchar(32) nullableyandex-eda, …
payment_type (расширить)добавить значение EXTERNAL_PREPAID
cancelled_by_customerboolean default falseдля отслеживания отмен гостем

Плюс новые статусы в enum order_status: HANDED_OVER, AWAITING_COURIER.

tip_events — входящие чаевые от Нетмонета (BR 3.2)

ПолеТипОписание
iduuid PK
binding_iduuid FK → bindings.idПривязка ТТ ↔ Нетмонет-заведение
providervarchar(32) default netmonetЗарезервировано на случай другого tip-провайдера
external_tip_idvarchar(100)ID транзакции в Нетмонет; UNIQUE (binding_id, external_tip_id) для идемпотентности
order_numbervarchar(20) nullableЕсли Нетмонет привязал к нашему заказу
table_numberinteger nullableЕсли QR был на столе
waiter_netmonet_idvarchar(100) nullableID сотрудника в Нетмонет — резолв в наш employees.netmonet_profile_id
waiter_iduuid nullableРезолв → наш employee_id. NULL если не удалось связать (ручное распределение)
amountdecimal(10,2)Сумма чаевых
currencyvarchar(10) default RUB
received_attimestampВремя получения чаевых по данным Нетмонета
raw_jsontextПолный payload webhook для аудита
processed_attimestamp nullableВремя публикации tips.received в Kafka
created_attimestamp

Индексы: uq_tip_events_external_id UNIQUE (binding_id, external_tip_id), idx_tip_events_binding, idx_tip_events_waiter, idx_tip_events_received_at.

Ссылки