BR 3.3 — PayKeeper Adapter: P0 Pilot (минимальная платёжная петля)
Контекст
PayKeeper (PK) — наш партнёр по приёму платежей, фискализации 54-ФЗ и эквайрингу. Мы отказались от собственного POS (ADR-016), PK заменяет iiko+эквайер+ФН. Интеграция — новый микросервис erp-paykeeper-adapter как anti-corruption layer между ERP и PK JSON API.
Что закрыто на 2026-04-23:
- PK подтвердил cross-device flow через invoice API (
POST /change/invoice/preview/→ кассир видит заказ на терминале и принимает оплату) — см. PK-summary-answers #1. - PK подтвердил refund webhook (включается техподдержкой) — #2.
- Формат MD5-подписи informer’а и формат ответа
OK <md5(id+secret)>подтверждены боевым кодом nearbyKoala/Koala_TG_app. - Reconciliation через stuck-платежи +
repeatcnt— рекомендация PK #3. - Фискальные поля через
/info/receipts/bypaymentid/+ callback §8.13 — #4. - Смены ФН управляются кассой PK автоматически, не требуют API — #5.
Архитектура: подробно в Research — PayKeeper bridge architecture (621 строка). Эта BR — первая фаза (P0 — одна пилотная ТТ).
Фазинг:
- P0 (эта BR) — ручной онбординг 1 ЛК PK, базовая платёжная петля, приём informer + refund, догрузка фискалки.
- P1+ (будущие BR) — catalog sync в ims-api, user sync в ЛК PK, autoinstaller, post-sale receipt для delivery, SBP QR, 2-stage hold.
Блокеры на стороне PK, актуальные для P0 и нерешённые, документированы в gap-analysis. Для P0 они не блокируют (обходим через передачу cart в invoice).
Бизнес-требования
R1. Ручной онбординг ЛК PayKeeper (per-ЮЛ)
Владелец франшизы или партнёр добавляет интеграцию с PK через новый раздел админки «Интеграции → PayKeeper» в карточке юрлица.
Поля формы:
pk_server_host— URL ЛК, вид{tsp}.server.paykeeper.ru(валидация regex)pk_login— логин ЛКpk_password— пароль ЛК (шифруется на лету в БД)informer_seed— секретное слово для подписи webhook’ов (шифруется)
Действия админки:
- Сохранить — запись в
paykeeper_accounts - Проверить — попытка
GET /info/settings/token/через переданные креды → успех/ошибка - Получить URL webhook’ов — система показывает готовые URL’ы (
/pk-webhooks/informer/{account_id},/pk-webhooks/refund/{account_id},/pk-webhooks/receipt/{account_id}), которые владелец копирует в ЛК PK
Правила:
- Один ЛК PK = одно юрлицо (
legal_entity_idUNIQUE вpaykeeper_accounts) - После сохранения интеграция
status=active - Разрешено только владельцам: пермишен
integrations.manage
R2. Mapping ТТ ↔ терминал PK
В карточке ТТ (экран Stores) добавляется секция «PayKeeper» (видна если у ЮЛ есть активная интеграция PK):
- Поле
pk_terminal_id— вписывается вручную (берётся из ЛК PK) - Поле
pk_mpos_merchant_id— вписывается вручную - Один
pk_terminal_id= одна ТТ (UNIQUE)
Правила:
- Пока ТТ не имеет mapping’а — PK-оплата для этой ТТ невозможна (при попытке создать invoice — 422 INTEGRATION_NOT_CONFIGURED)
- Изменение mapping’а — требует пермишен
integrations.manage
R3. Создание инвойса на заказ (ERP → PK)
Когда кассир нажимает «Оплатить» на POS-устройстве (или клиент запрашивает оплату):
- Order Service → публикует
order.payment_requested(payload:order_id,store_id,cart,total,customer_phone?,customer_email?) - Адаптер consume →
POST /change/invoice/preview/в PK с:pay_amount= total заказаclientid= имя клиента или ID (опц.)orderid= packed bridge-ID =base64url("{store_id_short}:{order_number}")service_name= JSON{cart: [...], user_result_callback: null}client_phone,client_email— если есть
- Адаптер сохраняет
paykeeper_invoicesзапись (pk_invoice_id,pk_invoice_url) - Адаптер публикует
paykeeper.invoice.created(payload:order_id,pk_invoice_id,pk_invoice_url) - Order Service сохраняет
orders.pk_invoice_id,orders.pk_invoice_url, передаёт URL клиенту (для оплаты) или в терминал PK (для bar-flow)
Правила:
- Таймаут PK API = 30с. При недоступности — записать в
pk_outboxи ретраить. - Cart формируется по формату
PaykeeperPayment.fiscal_cart(см. Koala helper):[{name, price, quantity, sum, tax}]. Модификаторы добавляются как отдельные строки. taxдля каждой позиции — изproducts.vat_rate. Поле добавляется в Catalog (см. R9).
R4. Приём informer’а успешной оплаты (PK → ERP)
PK шлёт POST на https://erp-test.nirbi.ru/pk-webhooks/informer/{account_id} при успешной оплате.
Обработка:
- Парсинг body (URL-encoded form-data): 17 полей —
id, sum, clientid, orderid, key, service_name, client_email, client_phone, ps_id, batch_date, fop_receipt_key, bank_id, bank_payer_id, card_number, card_holder, card_expiry, bank_operation_datetime. - Валидация MD5-подписи:
md5(id + sum(2dp) + clientid + orderid + informer_seed) == key. При несовпадении → 400, логирование. - Dedup по
(account_id, pk_payment_id=id). Если уже обработан — сразу успех-ответ без публикации. - Unpack
orderid→store_id,order_number→ найтиOrderв Order Service. - Сохранить в
paykeeper_paymentsзапись (все поля +raw_informer_json). - Опубликовать Kafka
paykeeper.payment.received(payload:order_id,pk_payment_id,amount,payment_method(поps_id),paid_at,bank_id,card_last4). - Ответить PK:
200 OK "OK <md5(id + informer_seed)>"(plain text, именно этот формат).
Consumer в Order Service: paykeeper.payment.received → orders.status=paid, orders.paid_at=received_at, orders.payment_method=..., orders.pk_payment_id.
R5. Догрузка фискальных атрибутов
После получения informer’а адаптер асинхронно:
- Вызывает
GET /info/receipts/bypaymentid/?payment_id={id}в PK. - Для каждого чека (обычно 1 — sale) сохраняет в
paykeeper_receipts. - Если
status=success— публикуетpaykeeper.receipt.fiscalized(payload:order_id,pk_receipt_id,type,fpd,fnd,fn,rnkkt,shift_number,receipt_number,fop_receipt_key,fop_url).
Альтернатива: если от PK подключён callback §8.13 — адаптер получает фискалку push’ом на /pk-webhooks/receipt/{account_id} (HMAC-SHA256 валидация). Поведение ниже одинаково.
Consumer в Order Service: paykeeper.receipt.fiscalized → order_payments.fiscal_data = {fpd, fnd, fn, rnkkt, shift_number, receipt_number}, order_payments.pk_fop_receipt_key.
R6. Приём webhook’а возврата
PK шлёт POST на /pk-webhooks/refund/{account_id} когда возврат (полный или частичный) выполнен.
Обработка:
- Парсинг полей (такой же список как informer).
- MD5-валидация (та же формула).
- Dedup по
(account_id, pk_payment_id, refund_sequence). - Unpack
orderid→ поискOrder. - Запись в
paykeeper_refunds(amount,status=done,datetime). - Публикация
paykeeper.payment.refunded(payload:order_id,pk_payment_id,amount,refund_total_so_far,is_full_refund). - Ответ PK:
OK <md5(id + secret)>.
Consumer в Order Service: paykeeper.payment.refunded → создание refund_records записи (если не существует), status=done.
R7. Инициация возврата из админки (ERP → PK)
Когда сотрудник с пермишеном orders.refund инициирует возврат через Order Service:
- Order Service → публикует
order.refund_requested(payload:order_id,pk_payment_id,amount,partial,refund_cart?,reason,initiated_by). - Адаптер consume →
POST /change/payment/reverse/в PK сid=pk_payment_id,amount,partial,refund_cart=JSON(...). - PK отвечает
{"result":"success"}— возврат запущен async. - Через N минут PK шлёт webhook возврата (R6).
Правила:
- Если PK возвращает
result=fail— публикуетсяpaykeeper.refund.failedсmsg. Order Service показывает ошибку в UI. - Для частичного возврата
refund_cartобязателен (форматPaykeeperPayment.fiscal_cart).
R8. Per-invoice poll как fallback
После paykeeper.invoice.created адаптер планирует проверку:
- Запись в
pk_invoice_check_scheduleсnext_run_at = now + 25 min,interval_min = 5. - Worker каждые 5 мин:
GET /info/invoice/byid/?id={pk_invoice_id}→ еслиstatus=paidиpaymentid != null— догружает/info/payments/byid/и публикуетpaykeeper.payment.received(если informer ещё не пришёл). - Удаление записи при успехе, отмене заказа или по TTL инвойса.
R9. Minimal поля в Catalog для PK
В catalog.products добавляются поля (обязательны для формирования fiscal_cart по 54-ФЗ):
vat_rateenum:none | vat0 | vat10 | vat20 | vat110 | vat120(defaultvat20)payment_subjectenum:goods | service | work | excise | job | payment | agency | composite | another(defaultgoods)payment_typeenum:full | prepay | advance | partial_prepay | ...(defaultfull)
Эти поля доступны для редактирования в админке в карточке товара (секция «Фискальные атрибуты», видна если у ЮЛ активна PK-интеграция).
R10. Ночной reconciliation cron
Каждую ночь в 03:00 MSK per-аккаунт:
GET /info/payments/bydate/?status[]=stuck&start=yesterday&end=today- Для каждого stuck’а —
POST /change/payment/repeatcnt/(просим PK ретайнуть оповещение) - Если через 10 мин informer не пришёл — ручная догрузка через
/info/payments/byid/?advanced=true+ публикацияpaykeeper.payment.receivedс пометкойreconciled=true
Логирование в pk_reconciliation_log.
Ролевая модель
| Роль | Действия |
|---|---|
Владелец франшизы (integrations.manage) | Создание/редактирование PK-аккаунтов, mapping ТТ ↔ терминал, просмотр логов |
Владелец партнёра (integrations.manage в рамках своих ЮЛ) | То же, только для своих ЮЛ |
| Менеджер ТТ | Просмотр: виден статус интеграции, последние платежи |
| Кассир | Невидимо (интеграция работает прозрачно через POS BFF) |
| Adapter (service) | Internal API endpoints с service token |
Не в скоупе P0
Явно не делаем в этой BR:
- Sync каталога ERP → PK ims-api — cart передаётся в invoice напрямую, стандалонного каталога в PK пока не поддерживаем. Phase P2.
- Sync сотрудников ERP → ЛК PK — кассиры заводятся вручную в ЛК PK. Phase P1.
- Autoinstaller — ручной онбординг (создание ЛК у PK — вне ERP). Phase P3.
- Post-sale receipt для delivery-сценариев — в P0 только sale receipt. Phase P1+.
- SBP QR активация (§10.1-10.2) — не нужна для bar-flow, опц. для POS BFF позже.
- 2-stage hold (pre-auth + capture) — по умолчанию 1-stage. Phase P1+.
- Обратный webhook каталога PK → ERP — PK в планах, не реализовано.
- Привязка карт (§9) — не нужно для MVP.
- Выплаты OCT (
api-vyplat.yaml) — не нужно.
Зависимости
Что достроить в существующих сервисах
| Сервис | Что |
|---|---|
| Catalog Service | Поля products.vat_rate, payment_subject, payment_type + миграция + UI в админке (R9) |
| Order Service | Поля orders.pk_invoice_id, pk_invoice_url, pk_payment_id + поле order_payments.fiscal_data (JSONB: fpd, fnd, fn, rnkkt, shift_number, receipt_number) + поле order_payments.pk_fop_receipt_key. Kafka producer order.payment_requested, order.refund_requested. Kafka consumer paykeeper.payment.received, paykeeper.payment.refunded, paykeeper.receipt.fiscalized |
| Store Service | Поля stores.pk_terminal_id, pk_mpos_merchant_id (nullable UNIQUE) + UI в карточке ТТ |
| User Service | Пермишен integrations.manage в каталоге (если ещё нет) |
| Admin BFF + Web | Раздел «Юридические лица → Интеграции PayKeeper», секция «PayKeeper» в карточке ТТ, секция «Фискальные атрибуты» в карточке товара |
| Nginx | Роут /pk-webhooks/* → erp-paykeeper-adapter:3015 |
Новый сервис
erp-paykeeper-adapter — Spring Boot :3015, новая БД paykeeper_adapter_db, см. research §3-4 для структуры.
Критерии приёмки (acceptance)
- Онбординг (R1): в пустом ERP админ создаёт PK-аккаунт через форму, система проверяет креды, показывает 3 webhook URL для ввода в ЛК PK.
- Mapping (R2): в карточке ТТ вписывается
pk_terminal_id, сохраняется, отображается. - Создание инвойса (R3): в тестовой ТТ создаётся заказ → Order Service публикует event → адаптер создаёт invoice в PK (sandbox) → ответ с
invoice_urlвозвращается в Order Service. - Informer (R4): при оплате тестового инвойса → PK шлёт webhook → адаптер валидирует MD5 → публикует Kafka → Order Service закрывает заказ. Повторный webhook с тем же payment_id — не создаёт дубль.
- Фискалка (R5): через 30 сек после оплаты
order_payments.fiscal_dataзаполненоfpd/fnd/fn/rnkkt/shift_number/receipt_number. - Refund (R6+R7): сотрудник инициирует возврат в админке → Order Service публикует event → адаптер вызывает PK → через N минут PK шлёт refund webhook → Order Service помечает возврат как
done. - Per-invoice poll (R8): при искусственно отключённом informer’е заказ закрывается через 25-30 мин через поллинг.
- Reconciliation (R10): ночной cron находит stuck-платежи и вызывает
repeatcnt; при продолжительном stuck — догружает черезbyidи закрывает заказ.
Ссылки
- Research — архитектура моста
- BR 2.4 — вопросы к PK на встрече (memo)
- Ответы техподдержки PK на 7 вопросов
- PayKeeper JSON API — разделы 1-10
- Gap analysis ERP × PayKeeper
- ADR-016 — Plan B если PK не успевает
- Референс:
github.com/nearbyKoala/Koala_TG_app— Laravel, боевые формулы подписей