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:
order_numberв payload →Order.waiter_idtable_number→zal_tables.current_waiter_idwaiter_netmonet_id→employees.netmonet_profile_id- Не резолвится →
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 формат» ниже.
Deep-link формат tip-QR (подтверждено 2026-05-05 через автологин ЛК)
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 TipEvent | erp-aggregator-service | ✅ |
zal_tables.current_waiter_id + PATCH /tables/{id}/waiter | erp-store-service (миграция 009) | ✅ |
employees.netmonet_profile_id, netmonet_enabled | erp-user-service (миграция 025) | ✅ |
orders.waiter_id, orders.tip_amount | erp-order-service (миграция 009) | ✅ |
Permissions tips.read / tips.edit | erp-user-service PermissionCatalog | ✅ |
/tips страница (mock) + web/src/api/tips.ts | erp-admin | ✅ ждёт real backend |
| TablesSection «Назначить официанта» | erp-admin | ✅ работает с реальным PATCH /tables/{id}/waiter |
| Карточка Netmonet в IntegrationsSection | erp-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 endpoint | erp-user-service | ✅ (2026-05-05) |
phone, netmonet_enabled, netmonet_profile_id в /internal/users/{id} response | erp-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: секция «Нетмонет (чаевые)» в карточке сотрудника со статусом + код + suspend | erp-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_INCOMPLETE422 —/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 заблокированы:
NetmonetWebhookControllerPOST/aggregator/netmonet/tips/newNetmonetConnector.parsePayload()(сейчас stubUnsupportedOperationException)- HMAC validation
- Резолв waiter_id (4 пути)
- Kafka publisher
tips.received TipsEventConsumerв Order Service- Backend
/internal/aggregators/tips - Замена mock в
/tipsUI
Action item: Email в support@netmonet.co с запросом контракта. Партнёрский UUID — см. memory.
Затрагиваемые сервисы
- Aggregator Service —
NetmonetApiClient,NetmonetSyncService,InternalNetmonetSyncController,Binding,tip_events,NetmonetConnector(stub) - User Service —
employees.netmonet_*поля, Kafka producer,EmployeeEventPublisher,PATCH /internal/users/{id}/netmonet-profile, exposephoneв internal response - Store Service —
zal_tables.current_waiter_id+ endpoint - Order Service —
orders.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’а нет.
Ссылки
- Netmonet_UserFlow.drawio — user flow (4 канала + отображение)
- Overview — место расширения
- tip_events
- Events —
tips.receivedschema (заблокирован) - zal_tables —
current_waiter_id - Data Model —
employees.netmonet_* - orders —
waiter_id,tip_amount - Roles — permission-каталог
- netmonet.co | admin.pre-prod.netmonet.co | Teamly partner docs