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 — подключение ТТ к агрегатору
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
store_id | UUID | ссылка на нашу ТТ (FK логический — Store Service в другой БД) |
franchise_id | UUID | для мультитенантности |
provider | varchar(32) | yandex-eda, market-delivery, … |
external_restaurant_id | varchar(100) | ID ресторана в системе провайдера |
client_id_encrypted | text | OAuth client_id (AES encrypted at rest) |
client_secret_encrypted | text | OAuth client_secret |
commission_percent | decimal(5,2) | например 25.00 |
is_enabled | boolean | true = принимаем заказы |
connected_at | timestamp | когда первый раз подключили |
last_menu_sync_at | timestamp | последняя успешная отдача снапшота |
last_order_received_at | timestamp | последний принятый заказ |
deleted_at | timestamp | soft delete |
created_at / updated_at | timestamp | — |
Индексы:
- unique(
store_id,provider) wheredeleted_at is null— одна ТТ → одно подключение к каждому провайдеру - idx(
franchise_id,provider)
aggregator_logs — лог всех pull/push
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
binding_id | UUID | FK |
direction | varchar(10) | inbound / outbound |
operation | varchar(50) | menu_sync, stoplist_pull, order_new, order_cancel, status_push, … |
external_reference | varchar(100) | external_order_id если применимо |
status_code | int | HTTP status |
request_body | text | тело запроса (при outbound — что мы послали; при inbound — что получили) |
response_body | text | ответ |
duration_ms | int | время запроса |
created_at | timestamp | — |
Retention: 30 дней (cron на удаление).
menu_snapshots — кэш снапшота меню
Чтобы не пересобирать JSON меню на каждый pull — кэшируем с TTL.
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
binding_id | UUID | FK |
snapshot_json | text | готовый ответ для агрегатора |
snapshot_hash | varchar(64) | SHA256 для дедупа |
built_at | timestamp | когда пересобрали |
expires_at | timestamp | built_at + 1 час |
При событии catalog.updated / product.availability.changed — инвалидация (ставим expires_at = now).
incoming_orders — реестр принятых заказов
Для идемпотентности и отладки.
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
binding_id | UUID | FK |
external_order_id | varchar(100) | ID в системе провайдера |
status | varchar(20) | accepted, rejected, duplicate |
order_id | UUID | наш Order Service order_id (nullable до создания) |
payload_json | text | raw payload от провайдера |
received_at | timestamp | — |
unique(binding_id, external_order_id) — дедуп.
status_push_queue — очередь исходящих push-обновлений
Если push упал, retry.
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
order_id | UUID | наш order |
binding_id | UUID | FK |
target_status | varchar(20) | accepted, cooking, ready, handed_over, rejected, cancelled |
reason | varchar(50) | для rejected |
attempts | int | 0..max |
next_attempt_at | timestamp | когда следующая попытка (exponential backoff) |
done_at | timestamp | успешно отправлено |
last_error | text | — |
Worker с cron раз в минуту перебирает done_at is null and next_attempt_at <= now и push-ит.
webhook_subscriptions — подписки внешних клиентов (не-агрегаторов)
(Добавлено в BR 2.5)
Подписки внешних POS-систем (KOALa, KDS-фронты, аналитические сервисы) на события Order Service. Бизнес-спека: Webhook-подписки.
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
franchise_id | UUID | мультитенантность |
store_id | UUID | nullable; NULL = подписка на все ТТ франшизы |
subscriber_name | varchar(100) | koala, kitchen-display, analytics — человекочитаемое имя |
webhook_url | text | HTTPS URL куда POST-ить события |
secret_encrypted | text | секрет для HMAC-SHA256 подписи тела (AES-encrypted at rest) |
events | jsonb | массив типов событий: ["order.paid", "order.closed", ...] |
is_active | boolean | default true |
deleted_at | timestamp | soft delete |
created_at / updated_at | timestamp | — |
Индексы:
- unique(
franchise_id,store_id,webhook_url) WHEREdeleted_at IS NULL AND is_active = true - idx(
franchise_id) WHEREis_active = true
webhook_delivery_attempts — лог попыток доставки
(Добавлено в BR 2.5)
Аудит и retry-логика для webhook-доставки.
| Поле | Тип | Описание |
|---|---|---|
id | UUID | PK |
subscription_id | UUID FK | → webhook_subscriptions |
event_id | UUID | ID события из Kafka (для идемпотентности) |
event_type | varchar(100) | order.paid, etc. |
payload_hash | varchar(64) | SHA256 тела запроса |
attempt_number | int | 1..N |
http_status | int | HTTP-ответ подписчика (NULL если сетевая ошибка) |
error | text | текст ошибки |
next_attempt_at | timestamp | когда повторить; NULL если успех или dead-letter |
delivered_at | timestamp | когда успешно доставлено (2xx); NULL до первого успеха |
created_at | timestamp | — |
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 (новая)
| Поле | Тип |
|---|---|
id | UUID PK |
product_id | UUID FK → products |
provider | varchar(32) |
external_sku | varchar(100) |
price_override | decimal(10,2) nullable |
is_available | boolean default true |
unique(product_id, provider).
Catalog Service — products (добавляется поле)
| Поле | Тип |
|---|---|
available_in_aggregators | boolean default false — глобально, показывать ли в агрегаторах |
Store Service — stores.supports_aggregators (добавляется)
Для управления “ТТ может принимать заказы агрегаторов” на уровне ТТ.
Order Service — orders (добавляются поля)
| Поле | Тип | Описание |
|---|---|---|
channel | varchar(32) default INTERNAL | INTERNAL, YANDEX_EDA, … |
external_order_id | varchar(100) nullable | ID в системе агрегатора |
external_provider | varchar(32) nullable | yandex-eda, … |
payment_type (расширить) | добавить значение EXTERNAL_PREPAID | |
cancelled_by_customer | boolean default false | для отслеживания отмен гостем |
Плюс новые статусы в enum order_status: HANDED_OVER, AWAITING_COURIER.
tip_events — входящие чаевые от Нетмонета (BR 3.2)
| Поле | Тип | Описание |
|---|---|---|
id | uuid PK | |
binding_id | uuid FK → bindings.id | Привязка ТТ ↔ Нетмонет-заведение |
provider | varchar(32) default netmonet | Зарезервировано на случай другого tip-провайдера |
external_tip_id | varchar(100) | ID транзакции в Нетмонет; UNIQUE (binding_id, external_tip_id) для идемпотентности |
order_number | varchar(20) nullable | Если Нетмонет привязал к нашему заказу |
table_number | integer nullable | Если QR был на столе |
waiter_netmonet_id | varchar(100) nullable | ID сотрудника в Нетмонет — резолв в наш employees.netmonet_profile_id |
waiter_id | uuid nullable | Резолв → наш employee_id. NULL если не удалось связать (ручное распределение) |
amount | decimal(10,2) | Сумма чаевых |
currency | varchar(10) default RUB | |
received_at | timestamp | Время получения чаевых по данным Нетмонета |
raw_json | text | Полный payload webhook для аудита |
processed_at | timestamp nullable | Время публикации tips.received в Kafka |
created_at | timestamp |
Индексы: uq_tip_events_external_id UNIQUE (binding_id, external_tip_id), idx_tip_events_binding, idx_tip_events_waiter, idx_tip_events_received_at.