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-topicorder.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
Создать подписку внешнего клиента на события заказов.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
| Content-Type | application/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"
]
}| Field | Type | Required | Description |
|---|---|---|---|
franchise_id | uuid | yes | Подписка на события конкретной франшизы |
store_id | uuid | no | NULL = все ТТ франшизы |
subscriber_name | string | yes | Человекочитаемое имя (koala, kitchen-display) |
webhook_url | string | yes | HTTPS URL для POST |
events | array[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
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный service-token |
VALIDATION_ERROR | 400 | Битый payload (невалидный URL, пустой events) |
DUPLICATE_SUBSCRIPTION | 409 | Подписка с тем же (franchise_id, store_id, webhook_url) уже активна |
GET /internal/webhook-subscriptions
Список подписок.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
franchise_id | uuid | yes | Фильтр по франшизе |
store_id | uuid | no | Фильтр по ТТ |
is_active | bool | no | Default 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 не перевыпускается.
| Параметр | Значение |
|---|---|
| Auth | X-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).
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Response 204
GET /internal/webhook-subscriptions/{id}/deliveries
Лог попыток доставки для подписки — для отладки.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
since | datetime | no | С какого момента показывать (default: -24h) |
only_failed | bool | no | Только неудачные попытки |
limit | int | no | Max 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’а. Используется если подписчик починил свою инфраструктуру.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Response 202
{
"data": {
"id": "uuid",
"attempt_number": 8,
"next_attempt_at": "datetime"
}
}Ошибки
Стандартные коды ошибок:
400 VALIDATION_ERROR— кривой payload401 UNAUTHORIZED,403 FORBIDDEN404 BINDING_NOT_FOUND,404 RESTAURANT_NOT_FOUND409 DUPLICATE_ORDER— заказ с такимexternal_order_idуже получен422 ITEM_NOT_MAPPED— товар из заказа не найден в маппинге SKU422 ITEM_OUT_OF_STOCK— товар в стоп-листе502 AGGREGATOR_ERROR— push в Яндекс упал (их 5xx)