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
  • GET /api/v1/admin/kds/devices

    • Bearer JWT (kds.settings.edit)
    • Proxy → User Service GET /admin/kds/devices
  • 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
  • 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 ниже)
  • PATCH /api/v1/pos/orders/{id}/items/{itemId}/kitchen-status

    • Bearer JWT с kds.access
    • Proxy → Order Service
  • PATCH /api/v1/pos/orders/{id}/kitchen-status?station_id=...

    • Bearer JWT с kds.access
    • Proxy → Order Service
  • 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
  • GET /api/v1/pos/products/{id}/recipe

    • Bearer JWT с kds.access
    • Proxy → Warehouse Service GET /tech-cards?product_id={id} (агрегирует первую техкарту в response)
  • 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 → иначе 403 KDS_ACCESS_DENIED
    • Применяется к routes с префиксом /api/v1/pos/kds/* и KDS-action endpoint’ам
  • kdsDeviceMiddleware — для KDS routes:

    • Парсит X-Device-Id header
    • 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 check kds.access
    • Query: ?station_ids=A,B
    • При подключении:
      • Регистрирует subscriber в in-memory Map: { deviceId, franchiseId, storeId, stationIds, ws }
      • Stream-key: franchise:{franchiseId}:store:{storeId}:stations:{stationIdsCsv}
    • Heartbeat: ping каждые 25 сек; close при отсутствии pong > 60 сек
    • При close — удаление из subscribers Map

Kafka Consumers

Файл: bff/src/kafka/kdsConsumer.ts (новый).

  • Consumer kds-bff-orders подписан на топики:

    • order.created
    • order.cooking_started (== order.status_changed с new→accepted)
    • order.item.kitchen_status_changed
    • order.cancelled
    • order.ready

    Логика: для каждого события — найти всех subscribers с подходящими franchiseId + storeId, у кого хотя бы одна stationIds пересекается с kitchen_station_id события (или для общих событий — любой subscriber по этой ТТ). Послать через WS соответствующий event_type payload.

  • 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, KdsFranchiseSettings
    • Order, OrderItem (расширены полями kitchen_*)
    • WS event types: OrderCreatedEvent, OrderItemStatusChangedEvent, OrderCancelledEvent, DeviceRevokedEvent

Конфигурация

  • .env.example:

    • ORDER_SERVICE_URL=http://order-service:3005
    • CATALOG_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.json
    • INTERNAL_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 настроек)

Ссылки