Агрегаторы доставки — бизнес-спека

Расширено в BR 2.5

Сервис Aggregator Service (владелец модуля) в BR 2.5 расширен — теперь обрабатывает не только агрегаторов (Яндекс.Еда и т.п.), но и внешние POS-системы (KOALa и подобные локальные фронты) через отдельный механизм webhook-подписок. Отдельная спека: Webhook-подписки.

Что это

Модуль ERP, отвечающий за интеграцию с внешними системами. Изначально (BR 3.1 — Яндекс.Еда) покрывал только маркетплейсы агрегации: приём заказов с Яндекс.Еды / Market Delivery и публикация нашего меню туда. В BR 2.5 расширен до «интеграционного hub’а» и покрывает также внешние POS-системы — локальные фронты (KOALa, KDS-экраны) которые подписываются на события наших заказов.

Типы интеграций

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

ТипКтоПаттернЧто отличается
Aggregator (маркетплейс)Яндекс.Еда, Market Delivery, ChibbisOAuth-binding: pull меню, push заказов, push lifecycle обратноДвусторонний full lifecycle. Детали — эта спека.
External POS (внешний POS/фронт)KOALa, KDSWebhook-подписка: только push событий заказа наружуОдностороннее уведомление. Заказы создаются извне через публичный API Order Service. Детали — Webhook-подписки.

Оба типа живут в одном сервисе (Aggregator Service, порт :3013) — переиспользуем инфру retry / dead-letter / логирования.

Ключевые сущности

Канал (channel)

Источник происхождения заказа. Значения: INTERNAL (касса в ресторане), YANDEX_EDA, MARKET_DELIVERY, в будущем OWN_WEBSITE, OWN_MOBILE_APP, CHIBBIS, и т.д.

Канал указан у каждого заказа в Order Service (orders.channel). От канала зависит:

  • Нужна ли фискализация на нашем POS (для INTERNAL — да, для агрегаторов — нет, Яндекс сам фискализирует)
  • Какой процесс отмены (внутренний / через push в агрегатор)
  • Какая выручка учитывается (полная / за вычетом комиссии)

Внешний заказ (external order)

Заказ, созданный не у нас, а в системе агрегатора. Отличается от внутреннего:

  • Имеет external_order_id — ID в системе агрегатора
  • Имеет channel != INTERNAL
  • payment_type = EXTERNAL_PREPAID — оплата прошла на стороне агрегатора
  • Фискальный чек уже выдан агрегатором

Подключение (integration binding)

Пара «наша ТТ ↔ ресторан в агрегаторе». Атрибуты:

  • store_id (наш)
  • aggregator (YANDEX_EDA и т.д.)
  • external_restaurant_id (их)
  • client_id / client_secret (OAuth для push-запросов)
  • is_enabled (активно/выключено)
  • commission_percent (по договору, для учёта)

Одна ТТ может иметь подключения к нескольким агрегаторам одновременно.

Маппинг SKU

Соответствие нашего product_idexternal_sku агрегатора. Создаётся автоматически после первой синхронизации меню. Живёт в Catalog Service как таблица product_external_mappings.

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

П1. Подключение ТТ к агрегатору

  • Подключает владелец франчайзи (scope=legal_entity_ids или type=franchise у ЮЛ) с permission stores.edit.
  • Перед подключением ТТ должна быть одобрена агрегатором (у Яндекса — требования к рейтингу, категории).
  • client_id / client_secret вводятся вручную на экране «Интеграции» в карточке ТТ.
  • После сохранения происходит первая синхронизация меню — до её успешного завершения интеграция не считается активной.

П2. Синхронизация меню

  • Яндекс пуллит GET /aggregator/yandex-eda/menu?restaurant_id=X раз в сутки.
  • Мы отдаём JSON в формате Яндекса, построенный из нашего Catalog Service.
  • Товары с available_in_aggregator=false не попадают в снапшот.
  • Изменения в каталоге (добавлен товар, изменена цена) — видны Яндексу на следующий день. Для срочных обновлений — ручной re-sync.
  • Цена в снапшоте: если у товара есть price_override — берётся она, иначе — базовая цена прейскуранта ТТ.

П3. Стоп-лист

  • Яндекс пуллит GET /aggregator/yandex-eda/stoplist?restaurant_id=X каждые ~10 минут.
  • Товар попадает в стоп-лист, если:
    • Вручную установлен в стоп через BR 1.13
    • Warehouse Service сообщил product.stock.depleted (Kafka)
    • Флаг available_in_aggregator=false (но тогда и в меню его нет — дублирование)
  • Товар возвращается в меню — когда стоп снят / остаток > порога.

П4. Приём заказа

  • Яндекс отправляет POST /aggregator/yandex-eda/orders/new с полным payload.
  • Валидации до сохранения:
    • Ресторан существует и активен
    • Все external_sku найдены в маппинге (если хоть один нет — заказ отклоняется автоматически с кодом «item_not_found»)
    • Ни один товар не в стоп-листе (иначе — отклонение «out_of_stock»)
  • При успехе — Order создаётся со статусом NEW, payment_type=EXTERNAL_PREPAID, PayKeeper НЕ вызывается.
  • Нотификация на POS через существующий канал (WebSocket / long-poll OrderService → POS).

П5. Жизненный цикл заказа

stateDiagram-v2
    [*] --> NEW: Яндекс → приём
    NEW --> IN_PROGRESS: кассир "Принять"
    NEW --> CANCELLED: кассир "Отклонить" / гость отменил
    IN_PROGRESS --> READY: кассир "Готов"
    IN_PROGRESS --> CANCELLED: кассир/гость отмена
    READY --> HANDED_OVER: кассир "Передан курьеру"
    HANDED_OVER --> [*]
    CANCELLED --> [*]

Каждая смена статуса → push в агрегатор. Если push упал — retry 3× с экспоненциальной задержкой, потом — алерт в admin UI «требуется ручная синхронизация».

П6. Отмена и возврат

  • Гость отменяет в приложении Яндекса → Яндекс нам уведомление → Order → CANCELLED.
  • Если заказ в IN_PROGRESS/READY и уже начато приготовление — агрегатор удерживает часть суммы (по своим правилам) или компенсирует ресторану полностью (по договору). Наша система только фиксирует факт, финансовая сторона — вне нашей логики.
  • Если отменил кассир «Отклонить» — указывает причину (out_of_stock, too_busy, technical_issue), передаётся в агрегатор.

П7. PayKeeper не участвует в агрегаторных заказах

  • Заказ уже фискализирован агрегатором.
  • Если кассир ошибочно пытается пробить чек на PayKeeper — интерфейс кассы блокирует эту опцию (поле requires_fiscalization=false передаётся POS-у).
  • В отчётах: выручка от агрегаторов идёт отдельной строкой, не сумма PayKeeper Z-отчёта.

П8. Маркированные товары

  • Перед «Передан курьеру» — если в составе есть маркированные товары, показываем экран сканирования DataMatrix.
  • Коды складируются в Order Service (order_marked_items) для последующего отчёта.
  • Если код невалиден — кассир не может закрыть заказ (нужно сообщить в поддержку).

Ролевая матрица

ДействиеФраншизаФранчайзиМенеджер ТТКассир
Видеть заказы по всей сети
Видеть заказы своего ЮЛ / своих ТТ✓ (свои)✓ (своя ТТ)✓ (текущая ТТ)
Подключить агрегатор к ТТ✓ (своя ТТ)
Править price_override✓ (свои ТТ)
Управлять стоп-листом
Принять/отклонить заказ
Отчёты по агрегаторам

Связи с другими модулями

  • Catalog Service — источник меню, мастер-данные товаров и цен.
  • Store Service — источник ТТ, store_external_mappings таблица подключений.
  • Order Service — мастер заказов; все агрегаторные заказы сохраняются здесь с channel.
  • Warehouse Service — источник сигналов для автоматического стоп-листа.
  • PayKeeper — НЕ участвует в агрегаторных заказах (уже фискализированы).

Ссылки