Research — PayKeeper bridge/adapter architecture
Сжатые факты об устройстве моста ERP ↔ PayKeeper. Исходники: _reference/paykeeper/*.md (protocol JSON API), _assets/paykeeper/gap-analysis.md, _assets/paykeeper/*.yaml (OpenAPI — ims-api, lk-paykeeper, partner-api, autoinstaller, paykeeper-app-mpos, support-api, api-vyplat, applepay), _reference/paykeeper/PK-summary-answers.md (ответы техподдержки PK 2026-04-23), github.com/nearbyKoala/Koala_TG_app (Laravel-референс-интеграция с боевыми формулами подписей и полями informer’а — изучена 2026-04-23).
1. Актёры и периметр
ERP (наша система): Catalog, Store, Order, User, Warehouse, Customer, Aggregator сервисы + Admin/POS/Customer BFF. Истина для: каталог, прейскурант, остатки, клиенты, роли, заказы до оплаты.
PayKeeper (партнёр): mPOS-терминал, ФН/ОФД/54-ФЗ, эквайринг, личный кабинет (ЛК) для мерчанта, ИМС-каталог PK. Истина для: платежи, фискальные документы, смены ФН.
Мост (новый сервис, erp-paykeeper-adapter): anti-corruption layer + outbox + webhook-приёмник + reconciliation-кроны. НЕ содержит бизнес-логику ERP.
Внешние зависимости за периметром моста: ОФД (через PK), НСПК/банк-эквайер (через PK), «Честный знак» (через PK), Applepay/MirPay/YandexPay (через PK).
2. Поверхности интеграции
PK выставляет 10 публичных групп эндпоинтов (JSON API) + отдельные OpenAPI (ims-api каталог, autoinstaller онбординг, partner-api, paykeeper-app-mpos deep links, support-api).
2.1 Outbound (ERP → PK), синхронные
| Направление | Endpoint PK | Когда вызываем | Источник |
|---|---|---|---|
| Инициация оплаты | POST /change/invoice/preview/ + POST /change/invoice/send/ | Order Service: заказ готов к оплате | §3.4/3.5 |
| Прямая ссылка оплаты / QR | POST /change/sbpqr/activate / activatelnk | POS BFF: по кнопке «Оплата QR» | §10.1/10.2 |
| Капча pre-auth | POST /change/payment/capture/ | Order Service: завершение отложенного списания | §2.12 |
| Возврат | POST /change/payment/reverse/ | Order Service: возврат инициирован | §2.8 |
| Чек окончательного расчёта | POST /change/payment/post-sale-receipt/ | Aggregator Service: факт доставки заказа | §2.13 |
| Сброс счётчика оповещений | POST /change/payment/repeatcnt/ | Reconciliation cron: найден stuck | §2.9 |
| Запрос токена | GET /info/settings/token/ | До любого POST, TTL = 24ч | §6.1 |
| Привязка карты / безакцепт | POST /change/binding/execute/ | Опц. подписочные модели, не MVP | §9.6 |
| Добавление email-получателя отчётов | POST /change/organization/addreportemail/ | Онбординг | §5.3 |
| Управление пользователями LK | POST /change/user/add/update/delete/ | Синхронизация кассиров User Service ↔ ЛК PK | §5.6-5.8 |
| Установка informer URL / secret | POST /change/organization/setting/ (informer_url, informer_seed) | Автоонбординг | §5.1 |
| Каталог — товары | POST/PUT/DELETE /ims-api/products | Catalog Service изменил товар | ims-api.yaml |
| Каталог — теги | POST/PUT/DELETE /ims-api/tags | Catalog Service изменил категорию | ims-api.yaml |
| Автоматическая установка ЛК | POST /autoinstaller/api/requests | Регистрация новой ТТ-франчайзи | autoinstaller.yaml |
2.2 Outbound (ERP → PK), polling / batch
| Что тянем | Endpoint PK | Периодичность | Причина |
|---|---|---|---|
| Stuck-платежи (по которым webhook не дошёл) | GET /info/payments/bydate/?status[]=stuck | 1 раз в сутки + по требованию | §2.1 — рекомендация PK: основной fallback reconciliation |
| Счётчики для контрольной сверки | GET /info/payments/bydatecount/ | При подозрении на расхождение | §2.2 |
| Суммы по платёжным системам | GET /info/systems/sums/ | Ежедневный отчёт | §1.2 |
| Фискальные атрибуты чека | GET /info/receipts/byid/ или /bypaymentid/ | После получения payment.success webhook | §8.3/8.4 |
| Доп. параметры платежа (RRN, APPROVAL_CODE, CARD_NUMBER, 3DSECURE) | GET /info/options/byid/?id= | Если полей нет в webhook | §2.4 |
| Список возвратов по платежу | GET /info/refunds/bypaymentid/ | После инициации возврата → проверка статуса | §2.7 |
| Отчётность xlsx | GET /export/payments/ | По требованию бухгалтерии | §7.1 |
2.3 Inbound (PK → ERP)
| Событие | Способ | Включение | Подпись |
|---|---|---|---|
| Успешная оплата | POST на informer_url (из §4.3) | Штатно, включено | MD5 по формуле (см. §6.4) — legacy PK informer, подтверждено Koala |
| Завершённый возврат | POST на отдельный URL (Koala: /payment/refund-webhook) | Включается техподдержкой PK per-ЛК | MD5 та же формула, что и informer success |
| Чек дошёл до финального статуса | POST callback | §8.13 | HMAC-SHA256 по secret_word, payload = все поля из /info/receipts/byid/ + sign |
| Изменения каталога в ЛК | нет на 2026-04-23 | В планах PK (ответ на вопрос #6) | TBD |
| Смена ФН истекает | нет | PK отказал (ответ #5: «не требуется — касса сама закроет-откроет смену») | — |
Важно: informer success и refund webhook — разные URL, одинаковая схема подписи (MD5). Receipt callback §8.13 — другая схема (HMAC-SHA256). Три разных приёмника.
2.3.1 Поля informer success / refund webhook (полный список)
Боевой список из Koala_TG_app/Services/PayKeeper/Webhook/Webhook.php (то что PK реально присылает):
| Поле | Тип | Обяз. | Назначение |
|---|---|---|---|
id | string/int | Да | ID платежа в PK |
sum | float | Да | Сумма |
clientid | string | Да | Передано в invoice (у нас — ФИО клиента или user_id) |
orderid | string | Да | Наш bridge-ID (см. §7.2) |
key | string(32) | Да | MD5-подпись, ^[a-f0-9]{32}$ |
service_name | string | Нет | JSON с cart + callback или произвольное |
client_email | string | Нет | Email плательщика |
client_phone | string | Нет | Телефон плательщика |
ps_id | int | Нет | ID платёжной системы |
batch_date | string | Нет | Флаг 2-стадийной оплаты (есть → hold активен) |
fop_receipt_key | string | Нет | Ключ чека 54-ФЗ (hasReceipt) |
bank_id | string | Нет | ID привязки карты (hasCardBinding) |
bank_payer_id | string | Нет | ID плательщика в банке |
card_number | string | Нет | Маскированный номер карты (4000 11xx xxxx 1111) |
card_holder | string | Нет | Имя держателя |
card_expiry | string | Нет | Срок действия |
bank_operation_datetime | string | Нет | Точное время операции в банке |
2.4 Deep links (PK mobile app)
Схема paykeeper://mobile.app/... — используется POS BFF и клиентским приложением для инициации оплаты на мобильном терминале кассира. Спека: paykeeper-app-mpos.yaml. 7 операций: invoice, refund, payout, check_state, check_shift, open_shift, close_shift — но последние 3 PK рекомендует не использовать (§5 ответа: касса сама управляет сменой).
3. Архитектурная форма моста
3.1 Выделенный сервис erp-paykeeper-adapter (уже в скоупе — memory project_erp_scope)
- Tech: Java 21 + Spring Boot 3.x + PostgreSQL + Kafka. Паритет стека с другими бэкендами.
- Порт: свободный
:3015. - БД:
paykeeper_adapter_db. Почему отдельная — изоляция credentials PK, ACL по принципу database-per-service. - Kafka — транспорт входящих событий PK к остальным сервисам ERP.
3.2 Почему не embed в существующий сервис
- Aggregator Service — уже есть
WebhookDispatcher, но он исходящий (ERP → сторонние POS типа KOALa). Paykeeper — входящий трафик + исходящие команды. Разные каналы, разная ответственность. - Order Service — не должен знать про
/info/settings/token/и HMAC-схемы PK (ACL принцип). - Отдельный сервис = отдельные права на секреты (PK login/password, informer_seed, service-token) в envs + изоляция deployment’а под перезапуск без касания Order/Catalog.
3.3 Внутренние модули адаптера
| Модуль | Роль |
|---|---|
pk-client | HTTP-клиенты к JSON API (10 групп) + ims-api + autoinstaller + partner-api. Basic auth, token refresh, retries, rate limit |
webhook-receiver | Spring MVC контроллеры на публичные endpoints, HMAC-валидация, dedup по payment_id/receipt_id, запись в webhook_log |
outbox-dispatcher | Worker, забирает из pk_outbox → вызов pk-client → публикация результата в Kafka или retry |
event-publisher | Kafka producer для исходящих событий моста |
event-consumer | Kafka consumer для order.refund_requested, catalog.product.updated, user.employee.pos_access_changed → outbox |
reconciliation-scheduler | Cron: stuck-платежи, ежедневная сводка, фискальные атрибуты |
mapping-registry | Таблицы mapping’а: store_id ↔ pk_terminal_id, employee_id ↔ pk_user_id, product_id ↔ pk_product_id |
credentials-vault | Чтение зашифрованных учёток PK из БД (AES-GCM, ключ из env) |
4. Модель данных адаптера
paykeeper_accounts — per-франчайзи/per-legal_entity
id, franchise_id, legal_entity_id, pk_server_host,
pk_login, pk_password_enc, informer_seed_enc,
paykeeper_id, status, onboarded_at, last_token_at
paykeeper_terminals — per-ТТ, бывает >1 терминала на точку
id, account_id, store_id, pk_terminal_id, pk_mpos_merchant_id,
label, status
paykeeper_user_mapping
id, account_id, employee_id, pk_user_id, pk_login,
synced_at, permissions_hash
paykeeper_product_mapping
id, account_id, product_id, pk_product_id, pk_tag_id,
synced_at, hash
paykeeper_invoices
id, order_id, account_id, pk_invoice_id, pk_invoice_url,
pk_status (created|sent|paid|expired), created_at, paid_at
paykeeper_payments — приёмник informer success
id, order_id, pk_payment_id, pk_unique_id, pay_amount,
payment_system_id, status, bank_id (для привязки),
pending_datetime, obtain_datetime, success_datetime,
raw_informer_json, received_at
paykeeper_receipts
id, payment_id (FK нашего pk_payment), pk_receipt_id,
type (sale|refund|expense|expense-refund), is_post_sale,
is_correction, status, fpd, fnd, fn, rnkkt, shift_number,
receipt_number, fop_receipt_key, fop_url, ts,
contact, cart_json, error_json, received_at
paykeeper_refunds
id, order_id, payment_id, pk_refund_id, amount,
status (started|done|failed), initiated_by, reason, datetime
pk_outbox — исходящие команды в PK
id, account_id, op_type, payload_json, status,
attempts, next_attempt_at, last_error, created_at, sent_at
webhook_log — все входящие запросы PK, для аудита
id, account_id, endpoint, raw_body, headers_json,
signature_valid, dedup_key, processed_at, created_at
pk_token_cache — кэш токенов 24ч
account_id (PK), token, expires_at
5. Event topology (Kafka)
5.1 Адаптер публикует
| Topic | Когда | Payload ключевое |
|---|---|---|
paykeeper.payment.received | Пришёл informer success | order_id, pk_payment_id, amount, payment_method, paid_at, bank_id? |
paykeeper.payment.refunded | Пришёл callback возврата | order_id, payment_id, refund_id, amount, status |
paykeeper.receipt.fiscalized | Пришёл callback §8.13 status=success | order_id, pk_receipt_id, type, fpd, fnd, fn, rnkkt, shift_number, receipt_number, fop_receipt_key, fop_url |
paykeeper.receipt.failed | §8.13 status=rejected/failed/timeout | order_id, pk_receipt_id, error.type, error.message |
paykeeper.invoice.created | После /change/invoice/preview/ | order_id, pk_invoice_id, pk_invoice_url |
paykeeper.account.provisioned | Autoinstaller дорисовал ЛК | legal_entity_id, pk_server_host, paykeeper_id |
5.2 Адаптер потребляет
| Topic | Источник | Результат |
|---|---|---|
order.payment_requested | Order Service при checkout | Создание invoice PK |
order.refund_requested | Order Service (админка или POS) | Вызов /change/payment/reverse/ |
order.handed_over (delivery) | Order Service (BR 2.5) | Вызов /change/payment/post-sale-receipt/ |
catalog.product.updated | Catalog Service | Upsert в ims-api PK |
catalog.product.deleted | Catalog Service | Delete в ims-api PK |
catalog.category.updated | Catalog Service | Upsert tag в ims-api PK |
user.employee.permissions_changed | User Service | Upsert/delete в /change/user/* |
Outbox pattern: при получении consume-события сначала INSERT INTO pk_outbox, затем ack. Worker вытаскивает батчами.
5.3 Консьюмеры в других сервисах
| Topic | Consumer | Действие |
|---|---|---|
paykeeper.payment.received | Order Service | orders.status=paid, orders.paid_at, orders.payment_method |
paykeeper.payment.received | Warehouse Service | Если order.status уже ready — списание остатков (переход с order.completed, см. BR 2.5 §10.1) |
paykeeper.payment.received | Customer Service | Точечный пересчёт dynamic групп если customer_id |
paykeeper.receipt.fiscalized | Order Service | order_payments.pk_fop_receipt_key, fiscal_data (ФН/ФД/ФП) |
paykeeper.payment.refunded | Order Service | refund_records.status=done |
6. Аутентификация и подписи
6.1 Adapter → PK (исходящие)
- Basic HTTP Auth по каждому запросу:
Authorization: Basic base64(pk_login:pk_password)(§6.2). Секреты — изpaykeeper_accounts, расшифровываются AES-GCM при вызове. - Security token для всех POST (§6.1). TTL 24ч. Хранится в
pk_token_cache. При401/истечении — refresh черезGET /info/settings/token/. - JWT service-token (§6.3) — для API выплат (OCT) и некоторых sub-API. Отдельный endpoint
/info/settings/service-token/?service=oct. - Per-account креды — у каждого франчайзи/юрлица свой ЛК → свой base URL (
{tsp}.server.paykeeper.ru), логин/пароль/seed. Выбор аккаунта — поfranchise_id/legal_entity_id/store_idв исходящем запросе.
6.2 PK → Adapter (входящие)
- Publicly-exposed endpoints (три отдельных URL для трёх каналов):
POST /pk-webhooks/informer/{account_id}— успешная оплатаPOST /pk-webhooks/refund/{account_id}— завершённый возвратPOST /pk-webhooks/receipt/{account_id}— чек в финальном статусе (§8.13)
- Роутинг по
account_idв URL — наш подход. Альтернатива Koala: безaccount_idв URL, shop_id упакован вorderid(см. §7.2). Наш подход надёжнее при возможной коллизии order-ID между аккаунтами; подход Koala проще при single-endpoint. - Dedup:
(account_id, dedup_key), гдеdedup_key=payment_idдля informer/refund /receipt_idдля receipt callback. - Idempotency: повторный запрос с тем же
dedup_key→ возврат 200 (в правильном формате, см. §6.5) без повторной публикации в Kafka.
6.3 Хранение секретов
pk_password,informer_seed— AES-GCM в БД, ключ шифрования из env varPAYKEEPER_SECRETS_KEY(32 байта, загружается из K8s secret / docker secret на деплое).- Отдельная таблица для секретов не нужна (поля
_encвpaykeeper_accounts). - Laravel-аналог в Koala: каст
'encrypted'на поляхpk_pass,pk_secretмоделиIntegrationPayKeeper. Та же идея, другой стек.
6.4 Формула подписи informer success / refund (MD5 legacy)
Боевая формула из Koala_TG_app/Services/PayKeeper/Webhook/Webhook.php:
signatureString =
id
+ number_format(sum, 2, '.', '') // "1234.00" (всегда 2 знака, точка, без тысячных разделителей)
+ (clientid ?? '')
+ (orderid ?? '')
+ secret_word // informer_seed из §4.3
expected_key = md5(signatureString) // hex lowercase, 32 символа
valid = hash_equals(expected_key, request.key)
Java/Spring:
String sigString = id
+ new BigDecimal(sum).setScale(2, RoundingMode.HALF_UP).toPlainString()
+ Optional.ofNullable(clientid).orElse("")
+ Optional.ofNullable(orderid).orElse("")
+ secretWord;
String expected = DigestUtils.md5Hex(sigString); // Apache Commons Codec
boolean valid = MessageDigest.isEqual(expected.getBytes(UTF_8), key.getBytes(UTF_8));6.5 Формат ответа на informer / refund webhook
Обязателен именно этот формат, иначе PK считает доставку несостоявшейся и продолжает ретраить:
Content-Type: text/plain
Body: "OK " + md5(id + secret_word)
Пример: OK a1b2c3d4e5f6... (префикс OK + пробел + 32 hex).
На ошибку валидации — plain-text описание без префикса OK (Koala возвращает "Hash mismatch", "Order not found", etc., PK ретраит до истечения лимита попыток).
6.6 Формула подписи receipt callback §8.13 (HMAC-SHA256 новая)
Это другая схема, только для callback’а о финальном статусе чека (не путать с informer):
params = {все POST-параметры кроме sign}
sorted_params = ksort(params) // лексикографически по ключам
to_hash = implode(';', sorted_params.values) // значения через точку с запятой
sign = hmac_sha256(to_hash, secret_word)
valid = strtoupper(sign) === strtoupper(request.sign)
7. Multi-tenancy
На уровне ERP: один franchise_id может содержать N юрлиц (BR 1.1), каждое юрлицо — N ТТ.
На стороне PK: один ЛК = один TSP = один набор креденшелов. Практически совпадает с «юрлицо». 1 ЮЛ → 1 ЛК PayKeeper → N терминалов.
7.1 Исходящее направление
paykeeper_accounts.legal_entity_id— FK на юрлицо из User Service.- Маршрутизация исходящего запроса:
store_id → paykeeper_terminals.account_id → paykeeper_accounts (creds + base_url).
7.2 Bridge-ID: упаковка store_id + order_id в orderid
Приём из Koala (OrderIdHelper::pack(order_id, shop_id) / unpack) — при создании invoice мы передаём в PK не чистый UUID заказа, а составной идентификатор, из которого на webhook’е можно восстановить и store, и order:
orderid = pack(store_id, order_id) // напр. base64url("<store_short>:<order_number>")
Позволяет:
- Маршрутизировать входящий webhook без
account_idв URL (если потребуется). - Найти нашу ТТ по
orderidдаже если PK пришлёт webhook с минимальным payload’ом. - Дедуплицировать через
(store_id, order_id)вместо глобальногоpayment_id.
У нас в URL webhook’а account_id остаётся (§6.2) для валидации подписи правильным seed’ом; orderid — вторичный bridge-ID внутри payload’а.
7.3 Входящее направление
- URL
{type}/{account_id}→ поискpaykeeper_accounts→ валидация подписи. orderidиз body →unpack→store_id,order_number→Orderв Order Service.
Граничный случай: ТТ поменяла юрлицо (перерегистрация) — нужен новый ЛК PK → новый paykeeper_account. Старые транзакции остаются в старом аккаунте. Свитч прозрачен для ERP (Store.legal_entity_id меняется, mapping обновляется).
8. Надёжность и согласованность
8.1 Проблемы
- PK может быть недоступен (сетевой лаг, плановые работы).
- Informer «стучится очень долго пока не дойдёт» (ответ PK на вопрос #3) — но может не дойти до нас совсем (например, адаптер upgrade).
- Webhook может дублироваться.
- Кассовое приложение может быть offline — чек в статусе
request_sentне перейдёт вsuccessдолго. - Возврат исполняется асинхронно — инициализация не гарантирует успех (§2.8).
8.2 Паттерны
| Паттерн | Где |
|---|---|
| Outbox | pk_outbox для всех исходящих команд, гарантия at-least-once |
| Idempotency keys | dedup по payment_id в informer, receipt_id в §8.13, pk_outbox.id в исходящих |
| Retry with backoff | Worker повторяет outbox с exp-backoff, cap 10 попыток, после — DLQ |
| Circuit breaker | Resilience4j, открытие при серии 5xx от PK, автоматическое закрытие через 60с |
| Rate limit (исходящий) | Токен-бакет на paykeeper_account_id, по умолчанию 10 RPS/ЛК |
| Reconciliation (дневной) | Cron: раз в сутки GET /info/payments/bydate/?status[]=stuck → POST /change/payment/repeatcnt/ для перезапуска оповещения |
| Reconciliation (per-invoice) | Приём из Koala: после создания инвойса планируется PaymentCheckSchedule с next_run_at = now + 25 min, интервал 5 мин. Поллит GET /info/invoice/byid/ до status=paid или до истечения TTL инвойса |
| Token cache | В отличие от Koala (которая запрашивает токен на каждый вызов) — кэш 24ч в pk_token_cache. Рефреш на 401 |
| Compensating action | Если outbox-попытка закончилась DLQ — нотификация в админку, ручной retry |
8.3 Consistency модель
- ERP → PK (каталог/пользователи): eventual consistency. Outbox = garantия доставки, но между записью в ERP и появлением в PK — лаг. Допустимо — ERP — source of truth, PK — реплика.
- PK → ERP (платежи/чеки): eventual consistency через informer + reconciliation. Окно расхождения — минуты/часы в худшем случае (до срабатывания reconcile).
- Фискальные атрибуты: могут прийти отдельным событием
paykeeper.receipt.fiscalizedпозже, чемpaykeeper.payment.received. Order Service должен уметь догружать их.
9. Каталог — deep sync
9.1 Что синхронизируется
| ERP → PK (ims-api) | Поле PK | Источник |
|---|---|---|
products.name | name | Catalog |
products.description | description | Catalog |
products.price (из прейскуранта ТТ) | price | Catalog + Store |
products.sku | sku | Catalog |
products.barcode | barcode | Catalog |
products.vat_rate (NEW) | tax | Catalog (нужно добавить — gap-analysis §2.1) |
products.payment_subject (NEW) | item_type | Catalog (NEW) |
products.is_marked | item_code_is_mandatory | Catalog |
products.unit_of_measure | measure (с маппингом через measure_pk_override) | Catalog |
products.tru_code (NEW, маркировка) | tru_code | Catalog |
categories.name → flat path | tag.name | Catalog (схлопывание дерева в строку) |
9.2 Известные ограничения PK (gap-analysis P0)
- Модификаторы не поддерживаются (P0) → workaround: передаём финальный
cartв deep link, кассир на mPOS не может выбирать модификаторы сам. - Иерархия категорий плоская (P0) → workaround: путь в имени тега.
- Per-store прейскуранты нет (P0) → workaround: актуальная цена в
cartкаждого заказа. - Availability per-store (стоп-листы) нет (P0) → workaround: скрыть товар во всех ТТ или не синкать в PK.
9.3 Следствие для моста
Полный каталог в PK — для случая standalone-терминала (кассир бьёт с нуля). Если сценарий «терминал только принимает оплату по готовому cart» — sync каталога не нужен, достаточно передавать cart.
Решение: два режима sync, управляется флагом paykeeper_accounts.catalog_sync_enabled. На MVP — выключен. При включении — батчевая заливка + инкрементальные updates из catalog.product.updated.
10. Онбординг (автоматизация)
10.1 Процесс
- Админ франшизы создаёт ЮЛ партнёра в ERP →
legal_entity.createdevent. - Адаптер consume’ит событие → вызов
POST /autoinstaller/api/requestsс данными ЮЛ (INN, shop name, email). - Поллинг
/autoinstaller/api/request_config_fiscal/get_statusдо статусаready. - Получены
pk_server_host({tsp}.server.paykeeper.ru),login,password. - Запись в
paykeeper_accounts, шифрование секретов. - Автоматический setup informer:
POST /change/organization/setting/сinformer_url=https://erp-test.nirbi.ru/pk-webhooks/informer/{account_id}+informer_seed=<рандом 32 байта>. - Добавление email отчётов, email поддержки.
- Публикация
paykeeper.account.provisioned.
10.2 Что PK подтвердил (ответ #7)
- Всё, что вручную в ЛК, автоматизируется через
4.*/5.*(/info/organization/*,/change/organization/*). - Рекомендация PK: первые внедрения делать вручную для выявления специфики. Автоматизация — Phase 2.
10.3 MVP подход
- Ручная регистрация ЛК + ручной ввод креденшелов в админке ERP (экран «Интеграции → PayKeeper» из gap-analysis §2.6).
- Адаптер только использует креды, не провижнит ЛК.
11. Отчётность и сверка
11.1 Источники правды
| Данные | Источник | Как получить |
|---|---|---|
| Сумма платежей за день по ТТ | PK | GET /info/systems/sums/?start=...&end=... |
| Количество платежей по статусу | PK | GET /info/payments/bydatecount/ |
| Реестр платежей (xlsx) | PK | GET /export/payments/ |
| Фискальные документы | PK | GET /info/receipts/bydate/ |
| Связь ERP order ↔ PK payment | ERP | order_payments.pk_payment_id (после BR из gap-analysis §2.3) |
11.2 Cron reconciliation
- Каждую ночь (03:00 MSK): per-аккаунт
GET /info/payments/bydate/?status[]=stuck&start=yesterday&end=today→ для каждого stuck’аPOST /change/payment/repeatcnt/→ если через 10 мин не прилетел webhook — ручная догрузка через/info/payments/byid/?id=&advanced=true+ публикацияpaykeeper.payment.receivedс пометкойreconciled=trueв payload. - Раз в неделю (понедельник 04:00): сверка сумм
GET /info/systems/sums/vsorders.totalв ERP → Slack-алерт при расхождении > 0.5%.
11.3 Per-invoice poll (приём из Koala)
Дополняет дневной cron — быстрое обнаружение успешной оплаты, если informer задерживается:
- После
createInvoice— запись вpk_invoice_check_schedule(order_id,account_id,pk_invoice_id,next_run_at = now + 25 min,interval_min = 5,type = invoice_poll). - Worker берёт задачи с
next_run_at <= now()→GET /info/invoice/byid/?id={pk_invoice_id}→ еслиstatus=paidиpaymentid != null→ подгрузкаGET /info/payments/byid/→ публикацияpaykeeper.payment.received(как будто пришёл informer). - Удалить запись при успехе или после TTL инвойса (
expiry_datetime).
Обоснование 25 мин: у Koala это время подобрано под их TTL. Для нас конфигурируется per-account.
12. Безопасность
12.1 Входящий трафик PK
- Endpoint
POST /pk-webhooks/{type}/{account_id}— публичен за nginx, HTTPS обязателен. - Валидация HMAC-подписи → 401 при невалидной.
- Rate limit per-account_id на nginx: 100 req/min (PK informer настырный, 10-30 попыток для одного платежа).
- IP whitelist — PK не гарантирует статичный IP, не рекомендуется.
- Логирование всех запросов в
webhook_log(raw body + headers + signature_valid) — retention 90 дней.
12.2 Исходящие данные
- В логах и Kafka — маскирование
CARD_NUMBER(обрезание до400011xxxxxx1111),informer_seed,pk_password. - PII (email, phone) — только в
webhook_log.raw_bodyиpaykeeper_payments.raw_informer_json, доступ только через service-account.
12.3 Audit
- Каждое исходящее P-изменение (create invoice, refund, user update) → запись в
pk_outbox+audit_log(who-what-when). - Доступ к
paykeeper_accounts— только с permissionintegrations.edit.
13. Deploy-артефакты
| Артефакт | Что |
|---|---|
erp-paykeeper-adapter repo | Spring Boot сервис |
| Docker Compose entry | :3015, env PK_SECRETS_KEY, PK_ADAPTER_DB_URL, Kafka brokers |
| Nginx route | /pk-webhooks/ → адаптер |
| DB migration | paykeeper_adapter_db с Liquibase changelog |
| Kafka topics (создать) | paykeeper.payment.received, paykeeper.payment.refunded, paykeeper.receipt.fiscalized, paykeeper.receipt.failed, paykeeper.invoice.created, paykeeper.account.provisioned |
| Админка | Новый раздел «Интеграции → PayKeeper» (gap-analysis §2.6) |
| Мониторинг | Метрики per-account: outbox queue size, webhook lag, retry count, 5xx rate |
14. Зависимости по сервисам
14.1 Блокеры ДО старта разработки адаптера
| Требование | Сервис | Ссылка |
|---|---|---|
Поля vat_rate, payment_subject, payment_type, tru_code в products | Catalog | gap §2.1 |
Kafka events catalog.product.*, catalog.category.* | Catalog | gap §2.1 |
| Outbox таблица | Catalog | gap §2.1 |
Kafka events user.employee.* + outbox | User | gap §2.2 |
Поля pk_payment_id, pk_fop_receipt_key в order_payments | Order | gap §2.3 |
Kafka consumer paykeeper.payment.received | Order | новое |
| Mapping store ↔ pk_terminal | Store | gap §2.4 |
14.2 Блокеры от PK (gap-analysis P0)
| Что ждём | Вопрос к PK | Текущий статус |
|---|---|---|
| Модификаторы в ims-api | 3.1 | Не решено |
| Иерархия категорий | 3.2 | Не решено |
| Per-store прейскуранты | 3.3 | Не решено |
| Availability per-terminal / стоп-листы | 3.4 | Не решено |
| Webhook на возврат | PK-summary #2 | Включается техподдержкой — снято, но сигнатура не в OpenAPI |
| Webhook на каталог | PK-summary #6 | «В планах PK» — открыто |
| Ролевая модель LK | 3.5 | Не решено — workaround через трансляцию флагов |
15. Phasing (факты, не рекомендации)
| Фаза | Что включает | Блокеры |
|---|---|---|
| P0 — Pilot одна ТТ | Ручной онбординг 1 ЛК PK + приём informer success + /info/receipts/byid/ догрузка фискалки + закрытие заказа в Order. Без sync каталога | Gap-analysis §2.3 (Order Service поля), §2.4 (Store ↔ terminal mapping) |
| P1 — Production | N точек, reconciliation cron, refund callback (после включения PK), админка «Интеграции», синк сотрудников в ЛК | §2.1 (Catalog outbox), §2.2 (User outbox) |
| P2 — Catalog sync | Заливка каталога в ims-api, поддержание consistency | §2.1 полностью + Catalog events |
| P3 — Автоонбординг | Autoinstaller + авто-провижининг informer | Ничего дополнительного от нас, обкатать на P1 |
| P4 — Deferred | Модификаторы, иерархия, per-store прейскуранты, availability | Блокеры на PK (gap §3.1-3.4) |
16. Открытые вопросы
Закрыто после анализа Koala_TG_app (2026-04-23):
Подпись informer — SHA1 или SHA256?→ MD5 (legacy schema), формула в §6.4. §8.13 HMAC-SHA256 — только для receipt callback.Формат ответа на webhook→OK <md5(id+secret)>plain text, §6.5.→ используем bridge-ID через packing (§7.2) — длина ≤100 символов проверена в боевом коде Koala.orderidдлинаRefund webhook существует?→ Да, отдельный URL, та же MD5-схема.
Открыто:
mpos_terminal_id— есть ли стабильный идентификатор терминала, который можно замапить наstore_id? В deep link есть, в informer неочевидно. Koala работает с 1 shop = 1 terminal, у них вопроса нет — у нас возможны >1 терминала на ТТ.- Поле
bank_id(привязки карт) — нужно ли нам хранить для возврата, или возврат по обычномуpayment_idдостаточен? - Кто владеет
informer_seed— мы генерим и пушим в ЛК через/change/organization/setting/?name=informer_seed, или он выдаётся PK? Оба варианта работают (Koala хранит готовый, не генерит). - Mapping товара ERP → PK: если кассир создал товар в ЛК (чего не должно быть по договорённости), что делаем? Игнорируем / тянем обратно / ругаемся в админку?
- Formatted amounts: PK возвращает суммы строкой с пробелами (
"304 755.00"), десятичный разделитель точка. Клиент моста нормализует черезreplace(" ","").replace(",",".")→BigDecimal. Koala используетnumber_format($n, 2, '.', '')на входе подписи. - 2-стадийный hold: Koala закомментировал
PaymentType::TWO_STAGE_HOLDи работает вONE_STAGE. Когда включать hold у нас?
17. Что уже есть в vault (reusable)
_reference/paykeeper/— 10 разделов JSON API, 2000+ строк._reference/paykeeper/PK-summary-answers.md— ответы поддержки на 7 вопросов._assets/paykeeper/gap-analysis.md— 354 строки, стыковка + разрывы._assets/paykeeper/*.yaml— 9 OpenAPI-спек (ims-api, lk-paykeeper, partner-api, autoinstaller, mpos-deep-links, support-api, api-vyplat, applepay, mirpay)._assets/paykeeper/vision-pos-platform.md,partnership-roadmap-2026.md,business-summary.md— бизнес-контекст.
18. Референс Koala_TG_app — что переиспользуем
Laravel-интеграция github.com/nearbyKoala/Koala_TG_app (PHP, iiko + PayKeeper для Telegram-доставки). Изучена 2026-04-23. Контекст другой (1 магазин = 1 ЛК, iiko-первопричина, Telegram-UI), но PK-коннектор и формулы подписей — боевые, портируем.
18.1 Что переиспользуем напрямую
| Артефакт Koala | Что берём | Как адаптируем |
|---|---|---|
PayKeeperApiService.php (9 методов) | Полный список сигнатур методов, payload’ы, обработка 401, retry-политика | Переписываем 1:1 на Java/Spring + RestClient/WebClient. Таблица эндпоинтов — см. §2.1 |
Webhook::verifySignature() формула MD5 | Точная формула подписи (§6.4) + формат ответа OK <md5(id+secret)> (§6.5) | Прямая реализация в WebhookReceiver адаптера |
Webhook поля парсинга | Полный список полей informer’а (17 штук — §2.3.1) | DTO InformerWebhookDto в адаптере с этими полями |
OrderIdHelper::pack/unpack | Bridge-ID подход — orderid = base64url(store_id:order_number) (§7.2) | Реализуем как утилитарный класс в общей либе BFF/адаптер |
ImsApiService.php (6 методов каталога) | JWT-auth через /info/settings/service-token?service=ims, методы addProduct/getProduct/getProducts/updateProduct/searchProduct/deleteProduct | Портируем при P2 phase (catalog sync) |
PaykeeperPayment.php helper | Референс от самого PK — формат fiscal_cart для 54-ФЗ (tax, quantity, sum, delivery, discounts) | Переиспользуем формат при сборке service_name.cart в invoice. Это PK’овая схема — она же нужна в любом адаптере |
PayKeeperStatus enum | ACL-статусы внутри адаптера | Переносим как есть: CREATED, PENDING, PAID, REFUNDING, CANCELLED, CAPTURING, CAPTURED, BOUND |
PaymentCheckSchedule + next_run_at=+25min, interval=5min | Per-invoice poll (§11.3) | Реализуем как Kafka Scheduled Tasks / Quartz |
IntegrationPayKeeper migration — pk_url, pk_user, pk_pass (encrypted), pk_secret (encrypted) | Структура хранения креденшелов + шифрование | В наших paykeeper_accounts — поля _enc с AES-GCM, та же идея |
Два endpoint’а: POST /payment/webhook, POST /payment/refund-webhook | Подтверждение что refund callback работает на той же MD5-схеме | У нас — /pk-webhooks/informer/{account_id}, /pk-webhooks/refund/{account_id}, §6.2 |
Retry-политика HTTP: retry(3, 1000ms, onConnectionException|timeout) | Базовая стратегия | Resilience4j RetryRegistry с теми же параметрами |
18.2 Что НЕ берём
| Решение Koala | Почему не берём |
|---|---|
| Токен запрашивается на каждый вызов | Wasteful. У нас — pk_token_cache TTL 24ч, refresh на 401 |
| Один магазин = один PK creds | У нас мульти-ТТ / мульти-терминал на ЮЛ. Разделяем paykeeper_accounts ↔ paykeeper_terminals |
| Прямые HTTP-вызовы из Laravel Jobs без outbox | Риск потери команд при падении. У нас — pk_outbox + worker |
| Нет receipt callback §8.13 | Они не нуждаются в фискальных атрибутах (нет налоговой сверки в vertical’е). У нас — обязательно |
Нет /change/payment/post-sale-receipt/ | У них нет сценария «предоплата → доставка». У нас — Aggregator + BR 2.5 требует |
| Нет SBP QR (§10.1-10.2) | У них оплата только по ссылке. У нас POS BFF может потребовать QR-активацию |
Нет sync пользователей в ЛК (/change/user/*) | У них нет concept «кассир» — платит клиент Telegram. У нас — кассиры с pos.access |
PaymentType::TWO_STAGE_HOLD закомментирован | У нас 2-stage нужен для сценариев «холд → капча после выдачи» (BR 2.5 delivery) |
| MD5 как основа всего | HMAC-SHA256 для receipt callback (§8.13) обязателен, Koala его просто не использует |
18.3 Уточнения нашему плану после анализа Koala
- §6 Auth: формулы подписей переписаны с гипотетических HMAC-SHA1/256 на реально работающие — MD5 для informer/refund, HMAC-SHA256 для receipt callback (две разных схемы, три разных endpoint’а у нас — §6.2).
- §6.5 Response format:
"OK <md5(id+secret)>"обязателен — без правильного ответа PK будет бесконечно ретраить. - §7.2 Bridge-ID: добавлен packed
orderid = base64url(store_id:order_number)как вторичный идентификатор внутри payload’а PK (в дополнение кaccount_idв URL webhook’а). - §11.3 Per-invoice poll: добавлен как complement к дневному stuck-cron.
- §8.2 Retry: зафиксирован формат
3 попытки × 1с, только ConnectionException/timeout(базовый слой; поверх — Resilience4j для circuit breaker). - §2.3 Поля informer: расширен список реальных полей (17 штук, включая
fop_receipt_key,bank_id,bank_payer_id,batch_date,ps_id,card_*,bank_operation_datetime).
18.4 Ссылки на файлы Koala (для цитирования при имплементации)
app/Services/PayKeeper/PayKeeperApiService.php— основной клиентapp/Services/PayKeeper/Webhook/Webhook.php— парсер + валидация подписиapp/Services/ImsApiService.php— каталог (IMS)app/Helpers/PaykeeperPayment.php— fiscal_cart helper (PK official)app/Helpers/OrderIdHelper.php— bridge-ID pack/unpack (нужно прочитать отдельно, не читали полностью)app/Http/Controllers/PayKeeperController.php::webhook/refundWebhook— обработчикиapp/Http/Controllers/MerchantController.php::createDeliveryInvoice— boot flowdatabase/migrations/2026_02_09_125722_create_integrations_paykeeper_table.php— DB схемаapp/Enums/PayKeeperStatus.php— enum статусовroutes/api.php— маршрутизация