POS BFF — BR 5.1
Где живёт код
erp-pos/bff/(репоnearbyErp/erp-pos, монорепо с frozen mobile/, активный bff/). Стек: Node.js 22 + Fastify + TypeScript + kafkajs.
POS BFF получает новые KDS-routes (proxy в Order/Catalog/User Services), WebSocket gateway для live-обновлений KDS, и Kafka consumer на события заказов.
Что делаем
Зависимости
-
package.json— добавить:@fastify/websocket(для WS gateway)undici(для HTTP-клиента — уже есть в admin-bff, переиспользуем)
- Обновить
pnpm-lock.yaml
Routes (REST)
Файл: bff/src/routes/kds.ts (новый).
-
POST /api/v1/admin/kds/devices/register- Bearer JWT (с
kds.settings.edit) - Proxy → User Service
POST /admin/kds/devices/register - Forward auth header
- Bearer JWT (с
-
GET /api/v1/admin/kds/devices- Bearer JWT (
kds.settings.edit) - Proxy → User Service
GET /admin/kds/devices
- Bearer JWT (
-
PATCH /api/v1/admin/kds/devices/{id}→ User Service -
DELETE /api/v1/admin/kds/devices/{id}→ User Service -
GET /api/v1/admin/kds/settings- Bearer JWT (
kds.settings.edit OR catalog.read) - Proxy → Catalog Service
GET /admin/kds/settings
- Bearer JWT (
-
PATCH /api/v1/admin/kds/settings→ Catalog Service -
GET /api/v1/pos/kds/orders- Bearer JWT (с
kds.access) - Proxy → Order Service
GET /internal/orders/active-by-stations(X-Service-Token,franchise_idиз JWT, остальные query прокидываются) - Side-effect: при каждом запросе вызывается heartbeat в User Service (через middleware ниже)
- Bearer JWT (с
-
PATCH /api/v1/pos/orders/{id}/items/{itemId}/kitchen-status- Bearer JWT с
kds.access - Proxy → Order Service
- Bearer JWT с
-
PATCH /api/v1/pos/orders/{id}/kitchen-status?station_id=...- Bearer JWT с
kds.access - Proxy → Order Service
- Bearer JWT с
-
GET /api/v1/pos/kitchen-stations- Bearer JWT (любой авторизованный)
- Proxy → Catalog Service
GET /kitchen-stations
-
GET /api/v1/pos/kds/settings- Bearer JWT с
kds.access - Proxy → Catalog Service
GET /admin/kds/settings
- Bearer JWT с
-
GET /api/v1/pos/products/{id}/recipe- Bearer JWT с
kds.access - Proxy → Warehouse Service
GET /tech-cards?product_id={id}(агрегирует первую техкарту в response)
- Bearer JWT с
-
GET /api/v1/kds/updates/latest- Public (без auth)
- Прокси на наш CDN/бакет с manifest (
https://updates.nirbi.ru/kds/latest.json) или возврат hardcoded в P0 - Cache-Control: max-age=300 (5 мин)
Middleware
-
kdsAccessMiddleware— для KDS routes:- Проверяет JWT содержит permission
kds.access→ иначе 403KDS_ACCESS_DENIED - Применяется к routes с префиксом
/api/v1/pos/kds/*и KDS-action endpoint’ам
- Проверяет JWT содержит permission
-
kdsDeviceMiddleware— для KDS routes:- Парсит
X-Device-Idheader - Async вызывает
POST /internal/kds-devices/{deviceId}/heartbeatв User Service (fire-and-forget, не блокирует основной запрос) - Если получен 401
DEVICE_REVOKED→ возвращает 401 клиенту с тем же кодом
- Парсит
WebSocket Gateway
Файл: bff/src/ws/kdsGateway.ts (новый).
- Endpoint
WS /api/v1/pos/kds/stream:- Auth через query param
?token=<jwt>(WS-стандарт), permission checkkds.access - Query:
?station_ids=A,B - При подключении:
- Регистрирует subscriber в in-memory Map:
{ deviceId, franchiseId, storeId, stationIds, ws } - Stream-key:
franchise:{franchiseId}:store:{storeId}:stations:{stationIdsCsv}
- Регистрирует subscriber в in-memory Map:
- Heartbeat: ping каждые 25 сек; close при отсутствии pong > 60 сек
- При close — удаление из subscribers Map
- Auth через query param
Kafka Consumers
Файл: bff/src/kafka/kdsConsumer.ts (новый).
-
Consumer
kds-bff-ordersподписан на топики:order.createdorder.cooking_started(==order.status_changedсnew→accepted)order.item.kitchen_status_changedorder.cancelledorder.ready
Логика: для каждого события — найти всех subscribers с подходящими
franchiseId + storeId, у кого хотя бы однаstationIdsпересекается сkitchen_station_idсобытия (или для общих событий — любой subscriber по этой ТТ). Послать через WS соответствующийevent_typepayload. -
Consumer
kds-bff-devicesподписан на:user.kds_device.revoked
Логика: найти все WS-сессии с этим
device_id→ отправить{ event: "device.revoked" }→ close WS с code 4001.
Shared types
-
shared/src/types/kds.ts— типы для запросов/ответов:KitchenStation,KdsDevice,KdsFranchiseSettingsOrder,OrderItem(расширены полями kitchen_*)- WS event types:
OrderCreatedEvent,OrderItemStatusChangedEvent,OrderCancelledEvent,DeviceRevokedEvent
Конфигурация
-
.env.example:ORDER_SERVICE_URL=http://order-service:3005CATALOG_SERVICE_URL=http://catalog-service:3004(уже есть)USER_SERVICE_URL=http://user-service:3002(уже есть)WAREHOUSE_SERVICE_URL=http://warehouse-service:3008(уже есть в admin-bff, добавить в pos-bff)KDS_UPDATES_MANIFEST_URL=https://updates.nirbi.ru/kds/latest.jsonINTERNAL_SERVICE_TOKEN=<...>(уже есть)
-
bff/src/server.ts— undici Agent с keepAliveTimeout=1ms (как в admin-bff, чтобы избежать stale DNS)
Tests
- Unit:
- WS subscriber registration / unregistration
- Kafka event filtering by stationIds
- Integration:
- Mock Order Service + проверить что REST proxy работает с auth-forward
- WS connection + Kafka event → broadcast
Что НЕ делаем
- Не делаем REST polling fallback на сервере — это клиентская логика (KDS-приложение само переключается)
- Не делаем offline-режим — это в pos-bff не нужно (BR 5.3 будет на стороне клиента)
- Не реализуем consumer для
catalog.kds_settings.updated— это в P1 (live-push настроек)