BR 3.2 — Интеграция с Нетмонет (чаевые официантам через QR)

Единый документ по Нетмонет

Это единственный актуальный документ по интеграции с Нетмонет. Содержит всё: контекст, бизнес-требования, реализованные части, оставшиеся блокеры, deep-link формат, состояние синтетики. ADR-019 и POS Phase 5A — Netmonet outgoing декомпозиция переехали сюда.

Контекст

Нетмонет — сервис Альфа-Банка для безналичных чаевых через QR. Уже интегрирован с iiko / R-Keeper / YCLIENTS.

Решение по архитектуре (2026-04-22):

  • Получатель — официант (dine-in), у каждого стола назначенный официант
  • QR-канал — на физическом чеке PayKeeper после оплаты (R8 ниже) + статический QR на столе (мерч)
  • Архитектура — расширение Aggregator Service (новый provider netmonet), переиспользуем bindings, aggregator_logs, AggregatorConnector

Доступы и API (pre-prod)

  • Base URL: https://api.pre-prod.netmonet.co (prod — https://api.netmonet.co)
  • OpenAPI: /v3/api-docs (admin partners) + /v3/api-docs/data (workplaces, employees, reviews)
  • Partner credentials: см. memory (UUID + password)
  • ЛК Нетмонет pre-prod: https://admin.pre-prod.netmonet.co — телефон владельца + пароль as180401
  • Партнёрская дока: https://netmonet.teamly.ru/at/e0590e76-9ad7-45b4-94ac-481e66d40f87 (требует логин)

JWT: 24h TTL по docs (фактически ~10h). Кэшируем на 23h в NetmonetApiClient.

Бизнес-требования

R1. Подключение ТТ к Нетмонет

Владелец франшизы / партнёр подключает ТТ через раздел «Интеграции» → provider=netmonet с username/password partner-API. Используется существующий /internal/aggregators/bindings.

R2. Назначение официанта на стол

В карточке ТТ → «Столы» → менеджер назначает официанта (zal_tables.current_waiter_id). Назначение действует до смены или ручного перераспределения. Нужно для резолва waiter_id в чаевых без order_number.

R3. Получение и сохранение чаевых (заблокировано — нет webhook contract)

Нетмонет → webhook → Aggregator: HMAC-валидация → idempotent save в tip_events → Kafka tips.received → лог.

R4. Привязка чаевых к официанту

Приоритет источников waiter_id:

  1. order_number в payload → Order.waiter_id
  2. table_numberzal_tables.current_waiter_id
  3. waiter_netmonet_idemployees.netmonet_profile_id
  4. Не резолвится → waiter_id=null, флаг ручного распределения

R5. Админка — статистика чаевых (заблокировано — backend ждёт webhook)

/tips: таблица (дата, ТТ, официант, сумма), фильтры, сводка, топ официантов. Permission tips.read (фронт + mock-API готовы, real backend отложен).

R6. Обогащение Order

При order_number в webhook → TipsEventConsumer (Order Service) обновляет orders.tip_amount, orders.waiter_id.

R7. Права

  • tips.read — просмотр чаевых (по scope)
  • tips.edit — управление подключением и настройками выплат

R8. QR на физическом чеке (добавлено 2026-05-05)

PK печатает на чеке после оплаты + фискализации deep-link tip-QR на нашего официанта. Подтверждено Леонидом (PK), детали — в секции «Deep-link формат» ниже.

https://netmonet.co/tip/{code}            ← prod
https://pre-prod.netmonet.co/tip/{code}   ← pre-prod
  • {code} — 6-знач employees.netmonet_profile_id (per-employee) или groupCode (shared tips на команду бара/кухни)
  • Опциональный query ?o={N} — partner context, не обязателен (URL без него работает идентично)
  • Внутренний redirect на /qr/{code}/tip (individual) или /qr/{code}/groups/0 (shared)
  • Для unregistered employee → страница «Сотрудник не зарегистрирован»

API workplace: GET /api/manager/workplace/{wpId}/codes/fallback-and-group{fallbackCode, groupCode}. Для тестового workplace 60971 (Арбат-флагман) — groupCode=005507, fallbackCode=null.

Что нужно от PayKeeper (к согласованию с Леонидом)

  • Опциональное поле tip_qr_url (string) в API создания счёта/чека
  • Опциональное поле tip_qr_label (string) — подпись под QR (например, «Чай Анне»)
  • PK печатает QR-блок на чеке только если поля заданы (back-compat)
  • Расположение блока — под фискальным QR

Логика на нашей стороне (paykeeper-adapter): передаём tip_qr_url только при employees.netmonet_enabled=true && netmonet_profile_id != null. Иначе fallback на групповой groupCode (если ТТ настроена на shared tips), либо опускаем поле.

Реализовано (Волны 1-3 + 5A)

Волна 1 — БД + permissions + UI mock (2026-04-22)

ЧастьРепоСостояние
Таблица tip_events + индексыerp-aggregator-service (миграция 003)
Entity TipEventerp-aggregator-service
zal_tables.current_waiter_id + PATCH /tables/{id}/waitererp-store-service (миграция 009)
employees.netmonet_profile_id, netmonet_enablederp-user-service (миграция 025)
orders.waiter_id, orders.tip_amounterp-order-service (миграция 009)
Permissions tips.read / tips.editerp-user-service PermissionCatalog
/tips страница (mock) + web/src/api/tips.tserp-admin✅ ждёт real backend
TablesSection «Назначить официанта»erp-admin✅ работает с реальным PATCH /tables/{id}/waiter
Карточка Netmonet в IntegrationsSectionerp-admin

Волна 3 — Outgoing employee sync через partner API (2026-04-29 + 2026-05-05)

Бывший «Phase 5A — Netmonet outgoing». Теперь покрывается этим BR.

ЧастьРепоСостояние
NetmonetApiClient (auth с form-urlencoded body, register/phone/suspend/reset)erp-aggregator-service
NetmonetApiClient data API (workplaces, available-codes, employee by code)erp-aggregator-service(2026-05-05)
NetmonetSyncService (auto-register берёт первый available-code per workplace)erp-aggregator-service(2026-05-05)
InternalNetmonetSyncController (8 endpoints incl. workplaces, available-codes, auto-register)erp-aggregator-service(2026-05-05)
Kafka producer + EmployeeEventPublisher (user.employee.{created,updated,deactivated,reactivated})erp-user-service(2026-05-05)
PATCH /internal/users/{id}/netmonet-profile callback endpointerp-user-service(2026-05-05)
phone, netmonet_enabled, netmonet_profile_id в /internal/users/{id} responseerp-user-service(2026-05-05)
Admin BFF /api/v1/admin/netmonet/* (workplaces / register / suspend orchestration)erp-admin (bff)(2026-05-05)
UI: «Подключить к Нетмонет» + NetmonetRegistrationModal (ТТ→workplace→group→submit)erp-admin (web)(2026-05-05)
UI: секция «Нетмонет (чаевые)» в карточке сотрудника со статусом + код + suspenderp-admin (web)(2026-05-05)
Deep-link QR формат подтверждёнresearch(2026-05-05)

E2E синтетика (2026-05-05):

  • Анна Кассирова → подключена через нашу UI → в Нетмонет workplace 60971 «Арбат-флагман», code 005509
  • Иван Управляев → подключён, code 005510
  • Suspend Анны → код возвращается в available-codes, в БД netmonet_enabled=false
  • Guest tip page (https://pre-prod.netmonet.co/tip/005509) рендерит карточку сотрудника

Известные баги e2e (2026-05-05)

  • Nginx 504: запрос POST /api/v1/admin/netmonet/employees/.../register иногда занимает >60s (auth+list+register+callback) → nginx timeout. Бэкенд успевает, но клиент получает 504. Решение: распараллелить шаги в BFF либо увеличить proxy_timeout. Сейчас не critical — бэкенд транзакционно консистентен.

Зафиксенные баги в этой итерации

  • EMPLOYEE_INCOMPLETE 422 — /internal/users/{id} не отдавал phone. Фикс: добавил phone, netmonet_enabled, netmonet_profile_id в buildInternalResponse().
  • UI «Сначала назначь сотрудника на ТТ» — employee.stores отсутствует в response. Фикс: stores из roles[].stores[].
  • NetmonetApiClient.fetchToken слал JSON вместо form-urlencoded (415 от Нетмонета). Фикс: MultiValueMap + APPLICATION_FORM_URLENCODED.

Заблокировано — Волна 2 (incoming tips webhook)

Нужен webhook-контракт от support@netmonet.co:

  • Формат payload (external_tip_id, amount, currency, received_at, order_number?, table_number?, employee_code?)
  • HMAC algorithm (SHA-256?) + header name (X-Netmonet-Signature?)
  • URL для регистрации webhook в их ЛК

Без contract заблокированы:

  • NetmonetWebhookController POST /aggregator/netmonet/tips/new
  • NetmonetConnector.parsePayload() (сейчас stub UnsupportedOperationException)
  • HMAC validation
  • Резолв waiter_id (4 пути)
  • Kafka publisher tips.received
  • TipsEventConsumer в Order Service
  • Backend /internal/aggregators/tips
  • Замена mock в /tips UI

Action item: Email в support@netmonet.co с запросом контракта. Партнёрский UUID — см. memory.

Затрагиваемые сервисы

  • Aggregator ServiceNetmonetApiClient, NetmonetSyncService, InternalNetmonetSyncController, Binding, tip_events, NetmonetConnector (stub)
  • User Serviceemployees.netmonet_* поля, Kafka producer, EmployeeEventPublisher, PATCH /internal/users/{id}/netmonet-profile, expose phone в internal response
  • Store Servicezal_tables.current_waiter_id + endpoint
  • Order Serviceorders.waiter_id, orders.tip_amount (consumer заблокирован)
  • Admin — UI секция «Нетмонет» в карточке сотрудника, BFF /api/v1/admin/netmonet/*
  • PayKeeper Adapter (Волна QR-на-чеке) — добавит tip_qr_url в invoice при наличии поля у PK API

Out of scope

  • Bill-pay через мерч-QR (полная оплата счёта через Нетмонет) — отдельная BR с фискализацией
  • Per-table waiter auto-clear на конец смены
  • Push-уведомление официанту через Notification Service
  • Налоговый отчёт по чаевым (Волна 4)
  • Dashboard топ-официантов, CSV-экспорт (Волна 4)
  • UI управления группами Нетмонет — пока через их ЛК
  • Создание workplaces в Нетмонет под Сокольники / Бауманскую (вручную в их ЛК)

User flow (4 канала + отображение)

Полная схема: Netmonet_UserFlow.drawio

5 этапов: setup сотрудника → 4 канала QR (чек, мерч, прямая ссылка, терминал) → гость платит → Aggregator резолвит waiter_id → отображение в 3 точках (ЛК Нетмонета, наша админка /tips, карточка заказа в POS).

Где видно чаевые

ГдеКто видитКто делает
ЛК Нетмонета (web + mobile)Сам официант (логин по телефону + SMS): сумма, дата, отзыв, звёзды, статус выводаНетмонет (внешний), мы не делаем
Наша Админка → /tipsФраншиза / Франчайзи / Менеджер ТТ (по scope + permission tips.read): сводная таблица, фильтры, топ-официантовМы (erp-admin); сейчас mock, real backend ждёт Волну 2
POS / отчёт по сменеКассир / менеджер: чаевые на конкретный заказ (orders.tip_amount) и итог по смене на официантаМы (Order Service + POS); срабатывает только если webhook принёс order_number

Канал «терминал PayKeeper»

По состоянию на 2026-05-12 в pre-prod API Нетмонета нет polling-эндпоинта для tips (/api/v1/partners/reviews отдаёт только отзывы без суммы — проверено через /v3/api-docs/data). Поэтому Волна 2 заблокирована именно на webhook contract, polling-fallback’а нет.

Ссылки