Интеграция PayKeeper — экраны управления
Архитектурный сдвиг — PayKeeper account 1:1 ↔ ТТ
До migration paykeeper-adapter#007: account был привязан к ЮЛ (1 ЛК PK = 1 ЮЛ, N терминалов под ним). После: account привязан к ТТ (1 ЛК PK = 1 ТТ). У одного ЮЛ может быть N разных PK-аккаунтов — по одному на каждую ТТ под этим ЮЛ. Бизнес-кейс: разные ТТ ведут оплату через разные эквайринговые договоры/банки.
Все настройки PK переехали с экрана ЮЛ на экран ТТ. Старая вкладка «PayKeeper» в карточке ЮЛ — заглушка с указанием куда настраивать.
Интеграция PayKeeper (приём платежей + фискализация 54-ФЗ) живёт в карточке ТТ:
- Карточка ТТ (
/stores/:id) — секция «PayKeeper». Управление аккаунтом: креды, webhook URLs, привязанный mPOS-терминал, suspend/delete.
Отдельной top-level страницы нет — embed’ится в карточку ТТ по аналогии с Интеграции.md.
1. Вкладка «PayKeeper» в карточке ЮЛ
Где расположена
В карточке /legal-entities/:id — новая вкладка «PayKeeper» рядом с «Реквизиты», «Владелец», «Права», «ТТ».
Видна только если у пользователя integrations.read.
Состояние «не подключено»
Empty state:
- Иконка + заголовок «Подключите PayKeeper»
- Пояснительный текст: «PayKeeper принимает платежи и формирует фискальные чеки 54-ФЗ для заказов этого юрлица. Один ЛК PK = одно юрлицо.»
- Кнопка «Подключить» (только при
integrations.manage) - Ссылка «Как получить доступ к ЛК PayKeeper?» → tooltip с объяснением онбординга
Форма подключения
Модальное окно «Подключить PayKeeper»:
- Хост ЛК (
pk_server_host) —text input, плейсхолдерexample.server.paykeeper.ru. Валидация regex на домен*.server.paykeeper.ru. - Логин (
pk_login) —text input. - Пароль (
pk_password) —password inputс кнопкой «глазик». - Секретное слово (
informer_seed) —password inputс «глазиком». Hint: «Секретное слово указывается в ЛК PayKeeper → Настройки → Уведомления». - Номер договора (
paykeeper_id, опционально) —text input.
Ссылка «Как это заполнить?» → tooltip с инструкцией.
Кнопка «Подключить и проверить»:
- Клиентская валидация.
- Отправка
POST /api/v1/admin/paykeeper/accounts. - Backend сразу делает тест-запрос
GET /info/settings/token/в PK → при успехе возвращает201+ URL’ы; при ошибке возвращает422 PK_CONNECTION_FAILEDс текстом от PK. - При успехе — модалка «Интеграция создана» (см. ниже).
- При ошибке — подсказка под формой с расшифровкой (неверные креды / таймаут / 500 от PK).
Модалка «Интеграция создана»
После успеха — блокирующая модалка (не даёт пропустить) со следующим содержанием:
Подключено ✓
Скопируйте 3 webhook URL в настройки ЛК PayKeeper:
Настройки → Уведомления
1. Успешная оплата:
https://erp-test.nirbi.ru/pk-webhooks/informer/{account_id}
[Копировать]
2. Возврат платежа:
https://erp-test.nirbi.ru/pk-webhooks/refund/{account_id}
[Копировать]
3. Чек 54-ФЗ (опционально):
https://erp-test.nirbi.ru/pk-webhooks/receipt/{account_id}
[Копировать]
Также укажите в ЛК тот же "informer_seed" что вы ввели выше.
Кнопка «Готово, всё настроил».
Состояние «подключено»
Карточка интеграции:
┌──────────────────────────────────────────────────────────┐
│ 🟢 PayKeeper [активен] │
│ │
│ ЛК: example.server.paykeeper.ru │
│ Логин: admin │
│ Номер договора: 140221-031-1 │
│ Подключено: 23.04.2026, 10:00 │
│ Привязано ТТ: 3 из 5 │
│ Последний токен: 23.04.2026, 14:30 │
│ │
│ [Webhook URLs] [Журнал] [Проверить] [Редактировать] │
│ │
│ [Приостановить] [Удалить] │
└──────────────────────────────────────────────────────────┘
Кнопки (видимость по permission):
| Кнопка | Кто видит | Действие |
|---|---|---|
| Webhook URLs | integrations.read | Модалка с 3 URL и «Копировать» |
| Журнал | integrations.read | Переход к подвкладке «Журнал» (см. ниже) |
| Проверить | integrations.manage | POST /.../test-connection → toast «Соединение работает» / «Ошибка: …» |
| Редактировать | integrations.manage | Модалка редактирования (те же поля что при создании; пароль и seed не pre-fill, обновляются только если введены) |
| Приостановить | integrations.manage | Модалка подтверждения, POST /.../suspend |
| Возобновить (видна если suspended) | integrations.manage | POST /.../resume |
| Удалить | integrations.manage | Модалка подтверждения. Если есть открытые инвойсы — блок и список |
Блок «Каталог в PayKeeper» (BR 3.4)
Отдельный блок внутри вкладки, ниже карточки интеграции.
Когда показан: только при status=active. При suspended / not_configured — скрыт полностью. Если интеграция есть, но suspended — выводится подсказка «Синхронизация каталога недоступна. Возобновите интеграцию, чтобы запустить sync.»
Mockup (active + успешный последний прогон)
┌─ Каталог в PayKeeper ──────────────────────────────┐
│ Товаров в ЛК PK: 142 ✓ │
│ (из 47 товаров ERP × модификаторы) │
│ │
│ Последняя синхронизация: 24.04.2026, 03:00 │
│ Триггер: ночной cron │
│ Статус: ✓ Успешно │
│ │
│ [Пересинхронизировать] [Журнал прогонов] │
└─────────────────────────────────────────────────────┘
Счётчик Товаров в ЛК PK — это общее число товаров в каталоге PayKeeper, включая развёрнутые из модификаторов варианты и addon’ы. Категории и группы модификаторов как отдельные сущности в PK не синхронизируются — категории попадают в имя товара префиксом, модификаторы раскрываются в отдельные товары (правило развёртывания).
Отображение статуса последнего прогона (last_run.status)
| Статус | Визуал | Поведение |
|---|---|---|
success | Зелёная галочка ✓ «Успешно» | — |
partial | Жёлтый треугольник ⚠ «Частично (N ошибок)» | Кликабельно → открывает «Журнал прогонов» с выделенной проблемной строкой |
failed | Красный крест ✗ «Не удалось» | Под статусом ссылка «Показать ошибку» → раскрывает last_error текстом |
running | Синий спиннер + «В процессе… Mm:ss» | Timer вычисляется как now - started_at. Polling 5 сек до перехода в финальное состояние |
| (нет прогонов) | Серый текст «Ещё не синхронизировался, нажмите «Пересинхронизировать»» | Кнопка «Журнал прогонов» скрыта |
Индикатор расхождений
Если totals.diverged_count > 0 — дополнительная оранжевая строка под основной:
⚠ N товаров в ERP не синхронизированы в PayKeeper.
Рекомендуется пересинхронизировать.
Tooltip на «⚠» уточняет: «Товар был изменён в ERP, но изменение не доехало до PayKeeper. Обычно происходит из-за проблем с сетью или временной недоступности PayKeeper API.»
Загрузка данных
GET /api/v1/admin/paykeeper/accounts/{id}/catalog-sync-statusпри монтировании вкладки.- Polling 30 сек если
last_run.status != running(для своевременного обновления блока). - Polling 5 сек если
last_run.status = running(для отображения прогресса). - При ошибке загрузки (500 / network) — fallback «Не удалось загрузить статус» с кнопкой «Повторить».
Кнопка «Пересинхронизировать»
| Параметр | Значение |
|---|---|
| Видна | Только при integrations.manage |
| Активна | Только при status=active интеграции и last_run.status != running |
| Action | POST /api/v1/admin/paykeeper/accounts/{id}/resync-catalog |
Поток:
- Клик → disable кнопки + spinner.
- При
202 Accepted→ toast «Синхронизация запущена», блок переходит вrunningсостояние (использует ответsync_run_idиstarted_at), начинается polling 5 сек. - При ошибках:
ACCOUNT_NOT_ACTIVE(422) → toast «Интеграция PayKeeper приостановлена. Возобновите, чтобы запустить синхронизацию».SYNC_ALREADY_RUNNING(409) → toast «Синхронизация уже идёт, дождитесь завершения». Блок сам подтянет running-состояние при следующем poll.FORBIDDEN(403) → toast «Недостаточно прав».
Кнопка «Журнал прогонов»
| Параметр | Значение |
|---|---|
| Видна | При integrations.read. Скрыта если прогонов не было. |
| Action | Открывает модалку |
Модалка «Журнал синхронизации каталога»
Таблица последних прогонов (GET /api/v1/admin/paykeeper/accounts/{id}/catalog-sync-runs?limit=20&since=...).
Колонки:
| Время | Триггер | Длительность | Статус | PK-товаров | Ошибок |
|---|---|---|---|---|---|
| 24.04, 03:00 | ночной cron | 8 сек | ✓ Успешно | +2 −0 | 0 |
| 24.04, 10:15 | manual | 12 сек | ⚠ Частично | +45 −0 | 2 |
| 23.04, 15:32 | webhook_missed | 3 сек | ✓ Успешно | +1 −0 | 0 |
- Формат счётчиков:
+upserted −deleted(прочерк если оба 0:−). - Длительность =
finished_at - started_at(дляrunning— «идёт N с»). - Триггер:
cron→ «ночной cron»manual→ «вручную»webhook_missed→ «восстановление» (tooltip: «Запущен автоматически, когда обнаружены несинхронизированные товары»)
Раскрытие строки → блок деталей:
Клик на строку раскрывает inline-блок (accordion):
GET /api/v1/admin/paykeeper/accounts/{id}/catalog-sync-runs/{run_id}→ получить полный объект.- Если
last_errorне null → красная плашка с текстом. - Если
errors_jsonне пуст → скроллируемый список (max-height 240px). Формат:{erp_product_id, variant_kind, variant_sku, message}. Рендерим как:
Имя товара резолвится через кэш из• Пицца Маргарита 30 см (base) [abc-123:size-30] Rate limited by PK, retrying via outbox • Пицца Маргарита 30 см доп. Сыр Чеддер (free_addon) [abc-123:size-30:+cheese-chr] PK API returned 500catalog-sync-status.totalsили дополнительный lookup (опционально). - Если ошибок нет — «Без ошибок» серым.
Пагинация:
- 20 на страницу, кнопка «Загрузить ещё» внизу (использует
since=lastSeenStartedAt).
Закрытие модалки — стандартная кнопка «Закрыть».
Подвкладка «Журнал»
Таблица последних 100 webhook-событий:
| Время | Endpoint | Подпись | Dedup key | Обработан | Заказ |
|---|---|---|---|---|---|
| 14:32:10 | informer | ✅ | 123456 | ✅ | #001 |
| 14:35:02 | informer | ✅ | 123457 | ✅ | #002 |
| 14:40:15 | informer | ❌ | — | — | — |
Фильтры:
- Endpoint: все / informer / refund / receipt
- Только с ошибкой подписи (checkbox)
- Период: последние 24ч / 7 дней / 30 дней
Клик по строке → модалка с полным raw body (JSON) и заголовками.
Пагинация: 20 записей, GET /api/v1/admin/paykeeper/accounts/{id}/logs?page=....
Состояние «приостановлено»
Карточка та же, но с красной меткой [приостановлено] и подсказкой «Новые платежи не принимаются. Существующие обрабатываются до закрытия».
Состояние «удалено»
Вкладка возвращается в состояние «не подключено». Исторические данные (paykeeper_payments, paykeeper_receipts) остаются в БД, но не видны через этот UI.
2. Секция «Терминал PayKeeper» в карточке ТТ
Где расположена
В карточке /stores/:id — новая секция «Терминал PayKeeper» (не отдельная вкладка, а секция на главной вкладке карточки ТТ).
Видна только если:
- У ЮЛ этой ТТ активная PK-интеграция (
paykeeper_accounts.status=active) - У пользователя
integrations.read
Иначе: секция скрыта полностью (не empty state, а просто нет).
Состояние «не привязана»
Под заголовком секции:
⚠ Терминал PayKeeper не привязан
ТТ не может принимать оплату через PayKeeper до привязки терминала.
[Привязать терминал] ← виден при integrations.manage
Форма привязки
Модальное окно «Привязать терминал PayKeeper»:
- ID терминала (
pk_terminal_id) —text input, из ЛК PK. - ID мерчанта (
pk_mpos_merchant_id) —text input, из ЛК PK. - Метка (
label, опционально) —text input, плейсхолдер «Касса 1».
Кнопка «Привязать»:
POST /api/v1/admin/paykeeper/terminalsсaccount_id(берётся автоматически из ЮЛ ТТ),store_id(из URL).- При
409 TERMINAL_STORE_EXISTS— «Эта ТТ уже привязана к другому терминалу». - При
409 TERMINAL_PK_ID_EXISTS— «Этот ID терминала уже используется другой ТТ».
Состояние «привязана»
✓ Терминал PayKeeper
ID терминала: TERM-001
ID мерчанта: MERCH-123
Метка: Касса 1
Статус: активен
[Редактировать] [Отвязать] ← видны при integrations.manage
Менеджер ТТ (только integrations.read) видит это же, но без кнопок — только info.
Отвязка
Модалка подтверждения: «Отвязать терминал? ТТ перестанет принимать оплату через PayKeeper».
При наличии открытых инвойсов (pk_status IN (created, sent)) — блок: «Нельзя отвязать — есть N открытых инвойсов».
3. Секция «Фискальные атрибуты» в форме товара
Где расположена
В карточке товара (/catalog/products/{id}) — таб «Информация», новая подсекция «Фискальные атрибуты».
Видна всегда (не зависит от активности PK-интеграции — поля в БД всё равно обязательны).
Поля
| Поле | Тип | Default | Описание |
|---|---|---|---|
Ставка НДС (vat_rate) | select | vat20 | Без НДС (none) / 0% (vat0) / 10% (vat10) / 20% (vat20) / 10/110 (vat110) / 20/120 (vat120) |
Предмет расчёта (payment_subject) | select | goods | Товар (goods) / Услуга (service) / Работа (work) / Подакцизный (excise) / Плата (payment) / Агентский (agency) / Составной (composite) / Иное (another) / Работа/услуга (job) |
Способ расчёта (payment_type) | select | full | Полный расчёт (full) / Предоплата (prepay) / Аванс (advance) / Частичная предоплата (partial_prepay) / Кредит (credit) / Оплата кредита (credit_pay) / Частичный расчёт (partial) |
Hint под секцией: «Используются при формировании фискального чека 54-ФЗ через PayKeeper».
При миграции существующих товаров — default-значения выставляются автоматически, не блокируют сохранение старых записей.
4. Кнопка «Вернуть» в карточке заказа (PK-flow)
Где расположена
В карточке /orders/:id — кнопка «Вернуть» в шапке, видна при:
order.statusIN (closed,delivered,ready/handed_over/in_deliveryсpaid_at)- Permission
orders.refund order.pk_payment_id != null(еслиnull— legacy-возврат, другой flow)
Модалка возврата
- Radio: «Полный возврат» / «Частичный возврат» (default — полный)
- Если частичный — список позиций с чекбоксами и количеством (max = оригинал)
- Поле «Причина» (обязательное, textarea)
- Итого к возврату (вычисляется автоматически)
Кнопка «Вернуть»:
POST /api/v1/admin/orders/{id}/refundс payload (реализовано в Order Service — оно публикуетorder.refund_requested).- Подтверждение: «Возврат запущен. Обновление статуса в течение нескольких минут».
- Toast с ID refund’а.
Отображение статуса возврата
В карточке заказа — секция «Возвраты»:
Возврат #1 — 500 ₽
Статус: в процессе... ← или Выполнен / Ошибка
Инициирован: 23.04.2026, 15:00, Иванов И.И.
Причина: Клиент отказался от заказа
[Подробнее]
Polling GET /api/v1/admin/orders/{id} каждые 10 сек пока хотя бы один RefundRecord в status=started. При обновлении до done / failed — уведомление + перестать поллить.
5. Состояния и ошибки
Ошибки PK-интеграции
| Код бэка | Что показать |
|---|---|
PK_CONNECTION_FAILED | «Не удалось подключиться к PayKeeper: {msg от PK}» |
ACCOUNT_EXISTS | «У этого юрлица уже есть PK-интеграция» |
TERMINAL_STORE_EXISTS | «Эта ТТ уже привязана» |
TERMINAL_PK_ID_EXISTS | «Этот ID терминала уже используется» |
ACCOUNT_HAS_OPEN_OPERATIONS | «Нельзя удалить: есть открытые инвойсы/возвраты» |
STORE_NOT_IN_SCOPE | «ТТ не принадлежит юрлицу этого PK-аккаунта» |
Ошибки возврата
| Код бэка | Что показать |
|---|---|
REFUND_NO_PK_PAYMENT | «У заказа нет платежа PayKeeper — возврат через legacy-процедуру» (с фолбэком на старый flow) |
REFUND_ALREADY_FULL | «Заказ уже полностью возвращён» |
REFUND_AMOUNT_EXCEEDS | «Сумма возврата превышает остаток по платежу» |
Ошибки синхронизации каталога (BR 3.4)
| Код бэка | Что показать |
|---|---|
ACCOUNT_NOT_ACTIVE | «Интеграция PayKeeper приостановлена — возобновите, чтобы запустить синхронизацию» |
SYNC_ALREADY_RUNNING | «Синхронизация уже идёт, дождитесь завершения» |
FORBIDDEN | «Недостаточно прав для запуска синхронизации» |
RATE_LIMITED (от Catalog Service /internal/catalog/full-snapshot) | «Слишком много запросов — повторите через {retry_after} сек» |
NOT_FOUND (на catalog-sync-runs/{run_id}) | «Прогон не найден» |
6. Permissions — сводка
| Действие | Permission |
|---|---|
| Вкладка «PayKeeper» в ЮЛ видна | integrations.read |
| Подключить / изменить / удалить PK-аккаунт | integrations.manage |
| Приостановить / возобновить | integrations.manage |
| Секция «Терминал PK» в ТТ видна | integrations.read |
| Привязать / отвязать / изменить терминал | integrations.manage |
| Проверить соединение | integrations.manage |
| Фискальные атрибуты товара редактируются | catalog.edit (существующий, не новый) |
| Инициировать возврат | orders.refund (существующий) |
| Просмотр журнала webhook’ов | integrations.read |
| Блок «Каталог в PayKeeper» виден | integrations.read |
| Кнопка «Пересинхронизировать каталог» | integrations.manage |
| Модалка «Журнал прогонов синхронизации» | integrations.read |
| Кнопка «Выгрузить из PK» (импорт сотрудников) (BR 3.5) | integrations.manage AND employees.edit |
| Журнал импортов сотрудников (BR 3.5) | integrations.read |
integrations.read и integrations.manage — уже существуют (переиспользуются из Webhook-подписки и Агрегаторы). Новые permissions не заводим — для всех фич BR 3.4 / BR 3.5 хватает существующих + employees.edit для импорта сотрудников.
7. Ссылки
- BR 3.3 — P0 Pilot
- BR 3.4 — Catalog Sync
- BR 3.5 — Импорт сотрудников
- Бизнес-спека PayKeeper
- Бизнес-спека · Catalog Sync
- Бизнес-спека · Импорт сотрудников
- Импорт сотрудников из PayKeeper (фронт-спека wizard’а)
- API контракты Paykeeper Adapter
- Интеграции агрегаторов (соседняя спека)
- Юридические лица — Карточка
- Каталог — Товары
- Заказы