Aggregator Service — API

Два контура эндпоинтов

Public (для агрегаторов) — защищены OAuth2 с их стороны. Admin (для Admin BFF) — защищены Bearer JWT ERP. Internal (для других наших сервисов) — X-Service-Token.

Public — что дёргает Яндекс / другой агрегатор

Префикс: /aggregator/{provider}/... где {provider}yandex-eda, market-delivery.

МетодПутьСтатусОписание
GET/aggregator/{provider}/menu?restaurant_id=XСнапшот меню для ресторана. Отдаётся JSON в формате провайдера.
GET/aggregator/{provider}/stoplist?restaurant_id=XТекущий стоп-лист (товары, временно недоступные).
POST/aggregator/{provider}/orders/newНовый заказ. Провайдер дёргает, передаёт полный payload. Идемпотентность по external_order_id.
POST/aggregator/{provider}/orders/{external_order_id}/cancel🕓 M3Гость отменил заказ в приложении провайдера. (не реализован)
GET/aggregator/{provider}/restaurants🕓 M3Список активных ресторанов этой франшизы, подключённых к агрегатору. (не реализован)
GET/aggregator/{provider}/restaurants/{restaurant_id}/schedule🕓 M3График работы ТТ для агрегатора. (не реализован)

Admin — для нашей админки

Префикс пути: /internal/aggregators/* (сервис) — Admin BFF проксирует его как /api/v1/admin/aggregators/*. Оба префикса ссылаются на один и тот же набор endpoints.

МетодПутьСтатусОписание
GET/internal/aggregators/bindings?store_id=XСписок подключений ТТ (по всем агрегаторам).
GET/internal/aggregators/bindings/{id}Детали подключения.
POST/internal/aggregators/bindingsПодключить ТТ к агрегатору. Body: { store_id, provider, external_restaurant_id, client_id, client_secret, commission_percent }.
PATCH/internal/aggregators/bindings/{id}Обновить credentials / commission / enabled.
DELETE/internal/aggregators/bindings/{id}Отключить ТТ от агрегатора (soft delete).
POST/internal/aggregators/bindings/{id}/test🕓 M3Тест-запрос к агрегатору для проверки credentials. (не реализован)
POST/internal/aggregators/bindings/{id}/resync-menu🕓 M3Ручная пересборка снапшота меню. (не реализован)
GET/internal/aggregators/logs?binding_id=X&since=YЛоги pull/push запросов по подключению.
GET/internal/aggregators/metrics?binding_id=X&period=Y🕓 M3Счётчики (заказов принято, отклонено, отменено, rejected_reasons). (не реализован)

Internal — для межсервисных вызовов

Префикс: /internal/aggregators.

МетодПутьОписание
GET/internal/aggregators/bindings/by-store/{store_id}Получить активные подключения ТТ (для Order Service, чтобы понять куда push-ить).

Push статусов → через Kafka, а не REST

Push статусов заказов агрегатору выполняет StatusChangedConsumer по Kafka-topic order.status.changed, а не через REST-endpoint. Ранее задокументированный POST /internal/aggregators/push-status не реализован и не планируется — используется чисто event-driven подход.

Структура данных (основные ответы)

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

{
  "id": "uuid",
  "store_id": "uuid",
  "store_name": "Ресторан на Тверской",
  "provider": "yandex-eda",
  "external_restaurant_id": "123456",
  "is_enabled": true,
  "commission_percent": "25.00",
  "connected_at": "2026-04-17T10:00:00Z",
  "last_menu_sync_at": "2026-04-17T06:00:00Z",
  "last_order_received_at": "2026-04-17T12:45:00Z"
}

Запрос на подключение

{
  "store_id": "uuid",
  "provider": "yandex-eda",
  "external_restaurant_id": "123456",
  "client_id": "yandex_client_abc",
  "client_secret": "yandex_secret_xyz",
  "commission_percent": "25.00"
}

Входящий заказ (Яндекс → нам)

Точная схема определится после получения договора с Яндексом. Ожидаемый вид:

{
  "external_order_id": "ye-2026-04-17-000123",
  "restaurant_id": "123456",
  "created_at": "2026-04-17T12:45:00Z",
  "estimated_pickup_at": "2026-04-17T13:10:00Z",
  "customer": { "first_name": "Иван", "phone_mask": "+7***1234" },
  "delivery": { "type": "courier", "address_encrypted": "..." },
  "payment": { "type": "online_prepaid", "status": "paid", "amount": 1250.00 },
  "items": [
    { "external_sku": "pizza-pepperoni-30", "name": "Пицца Пепперони 30см",
      "quantity": 1, "unit_price": 750.00,
      "modifiers": [{ "name": "Сырный борт", "price": 120.00 }]
    },
    { "external_sku": "cola-500", "name": "Кока-Кола 0.5", "quantity": 2, "unit_price": 190.00 }
  ],
  "totals": { "subtotal": 1250.00, "delivery": 0, "discount": 0, "total": 1250.00 },
  "comment": "Без лука"
}

Ответ на приём заказа

{ "status": "received", "internal_order_id": "uuid" }

или при отказе автоматической валидации:

{ "status": "rejected", "reason": "item_not_found", "details": "external_sku 'cola-500' not mapped" }

Авторизация

  • Public endpoints (агрегаторы → нам): OAuth2 resource server. Агрегатор получает access_token от нашего OAuth provider (выдаётся при создании binding), шлёт его в Authorization: Bearer ....
  • Admin endpoints: ERP JWT через Admin BFF.
  • Internal endpoints: X-Service-Token: <SERVICE_TOKEN>.

Webhook-подписки внешних клиентов (BR 2.5)

Секция добавлена в BR 2.5. Бизнес-спека: Webhook-подписки.

В MVP управление подписками — через internal endpoints с service-token (не публичный admin API). Admin-UI — отложено на Phase 2.

POST /internal/webhook-subscriptions

Создать подписку внешнего клиента на события заказов.

ПараметрЗначение
AuthX-Service-Token
Content-Typeapplication/json

Request Body

{
  "franchise_id": "uuid",
  "store_id": "uuid | null",
  "subscriber_name": "koala",
  "webhook_url": "https://koala-pos.example.com/webhooks/erp",
  "events": [
    "order.created",
    "order.paid",
    "order.ready",
    "order.closed",
    "order.cancelled"
  ]
}
FieldTypeRequiredDescription
franchise_iduuidyesПодписка на события конкретной франшизы
store_iduuidnoNULL = все ТТ франшизы
subscriber_namestringyesЧеловекочитаемое имя (koala, kitchen-display)
webhook_urlstringyesHTTPS URL для POST
eventsarray[string]yesТипы событий (см. Events)

Response 201

{
  "data": {
    "id": "uuid",
    "franchise_id": "uuid",
    "store_id": "uuid | null",
    "subscriber_name": "koala",
    "webhook_url": "https://...",
    "events": [...],
    "is_active": true,
    "secret": "generated-once-only-32chars-hmac-secret",
    "created_at": "datetime"
  }
}

secret возвращается только при создании

Секрет генерируется сервером криптостойким образом (min 32 символа). После этого он хранится зашифрованно и не может быть прочитан через API. Если клиент потерял — нужно пересоздать подписку или использовать endpoint rotate (Phase 2).

Errors

CodeHTTPDescription
UNAUTHORIZED401Невалидный service-token
VALIDATION_ERROR400Битый payload (невалидный URL, пустой events)
DUPLICATE_SUBSCRIPTION409Подписка с тем же (franchise_id, store_id, webhook_url) уже активна

GET /internal/webhook-subscriptions

Список подписок.

ПараметрЗначение
AuthX-Service-Token

Query Parameters

ParamTypeRequiredDescription
franchise_iduuidyesФильтр по франшизе
store_iduuidnoФильтр по ТТ
is_activeboolnoDefault true

Response 200

{
  "data": [
    {
      "id": "uuid",
      "franchise_id": "uuid",
      "store_id": "uuid | null",
      "subscriber_name": "koala",
      "webhook_url": "https://...",
      "events": [...],
      "is_active": true,
      "created_at": "datetime"
    }
  ]
}

Секрет не возвращается в этом endpoint’е (только при создании).


PATCH /internal/webhook-subscriptions/{id}

Обновить подписку (events, webhook_url, is_active). Secret не перевыпускается.

ПараметрЗначение
AuthX-Service-Token

Request Body

{
  "events": [...],              // optional
  "webhook_url": "https://...", // optional
  "is_active": false            // optional
}

Response 200

Полная запись (без секрета).


DELETE /internal/webhook-subscriptions/{id}

Soft-delete подписки. Заказы перестают доставляться. Исторические записи webhook_delivery_attempts остаются (до истечения retention).

ПараметрЗначение
AuthX-Service-Token

Response 204


GET /internal/webhook-subscriptions/{id}/deliveries

Лог попыток доставки для подписки — для отладки.

ПараметрЗначение
AuthX-Service-Token

Query Parameters

ParamTypeRequiredDescription
sincedatetimenoС какого момента показывать (default: -24h)
only_failedboolnoТолько неудачные попытки
limitintnoMax 1000, default 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "event_id": "uuid",
      "event_type": "order.paid",
      "attempt_number": 3,
      "http_status": 500,
      "error": "Connection timeout after 5s",
      "next_attempt_at": "datetime | null",
      "delivered_at": "datetime | null",
      "created_at": "datetime"
    }
  ]
}

POST /internal/webhook-deliveries/{id}/retry

Ручной retry dead-letter’а. Используется если подписчик починил свою инфраструктуру.

ПараметрЗначение
AuthX-Service-Token

Response 202

{
  "data": {
    "id": "uuid",
    "attempt_number": 8,
    "next_attempt_at": "datetime"
  }
}

Ошибки

Стандартные коды ошибок:

  • 400 VALIDATION_ERROR — кривой payload
  • 401 UNAUTHORIZED, 403 FORBIDDEN
  • 404 BINDING_NOT_FOUND, 404 RESTAURANT_NOT_FOUND
  • 409 DUPLICATE_ORDER — заказ с таким external_order_id уже получен
  • 422 ITEM_NOT_MAPPED — товар из заказа не найден в маппинге SKU
  • 422 ITEM_OUT_OF_STOCK — товар в стоп-листе
  • 502 AGGREGATOR_ERROR — push в Яндекс упал (их 5xx)

Ссылки