BR 3.3 — Paykeeper Adapter (новый сервис)
Источники
Задачи
Инициализация репо
- Создать репо
erp-paykeeper-adapterвnearbyErp/GitHub org - Добавить в
07-Tasks/Repositories.md - Скелет Spring Boot 3.x + Java 21, Maven, Lombok, Liquibase (шаблон по образцу erp-aggregator-service)
- Docker:
Dockerfile(multi-stage build, java-21-slim runtime) -
.claude/CLAUDE.mdс правилами изоляции (по образцу других сервисов)
Миграции БД
-
changelog/001-paykeeper-accounts-and-terminals.xml:paykeeper_accounts(id, legal_entity_id UNIQUE, pk_server_host, pk_login, pk_password_enc BYTEA, informer_seed_enc BYTEA, paykeeper_id, status, onboarded_at, last_token_at, timestamps, deleted_at)paykeeper_terminals(id, account_id FK, store_id UNIQUE, pk_terminal_id UNIQUE, pk_mpos_merchant_id, label, status, timestamps)- Индексы по §Data Model
-
changelog/002-invoices-payments-receipts-refunds.xml:paykeeper_invoices,paykeeper_payments,paykeeper_receipts,paykeeper_refunds
-
changelog/003-outbox-logs-schedulers.xml:pk_outbox,webhook_log,pk_token_cache,pk_invoice_check_schedule
Entities + Repos
- JPA entities для всех 10 таблиц
- Spring Data репозитории с custom queries для outbox worker’а и scheduler’а
Криптография секретов
-
SecretVaultutility — AES-GCM шифрованиеpk_password/informer_seedс ключом изPAYKEEPER_SECRETS_KEYenv - Entity-level
@Convertдля автоматической дешифровки при чтении (или явный метод — на выбор)
HTTP-клиент PayKeeper
-
PayKeeperClient— RestClient/WebClient с Basic auth + form-urlencoded POST + JSON response - Token cache: Redis key
pk:token:{account_id}TTL 24ч, refresh на 401 - Retry: 3 попытки × 1000ms на ConnectionException/timeout
- Circuit breaker: Resilience4j, open при серии 5xx
- Все 10 методов из Koala’s
PayKeeperApiService(§18.1 research):getAuthToken,createInvoice,getInvoiceById,sendInvoice,getPaymentById,reversePayment,changePaymentCapture,infoOptionsById,changeBindingExecute+ дополнительноgetReceiptsByPaymentId,repeatcnt,getPaymentsByDateдля reconciliation
Webhook-receiver (публичный)
- Controllers:
POST /pk-webhooks/informer/{account_id}— парсинг form-data вInformerWebhookDto(17 полей), MD5-валидация, dedup, publish Kafka, ответ"OK <md5(id+secret)>"POST /pk-webhooks/refund/{account_id}— тот же handler, другая публикацияPOST /pk-webhooks/receipt/{account_id}— HMAC-SHA256 валидация, парсинг полей receipt
-
SignatureValidator— MD5 по формуле research §6.4 + HMAC-SHA256 по §6.6 - Логирование всех запросов в
webhook_logнезависимо от исхода (for audit) - Rate limit per-account_id (Resilience4j)
Admin API
-
AccountController—/internal/paykeeper/accounts/*CRUD + suspend/resume/test-connection (см. API.md) -
TerminalController—/internal/paykeeper/terminals/*CRUD -
LogsController—/internal/paykeeper/accounts/{id}/logs -
InternalController—/internal/paykeeper/accounts/by-legal-entity/{id},/terminals/by-store/{id},/refunds/{id}/retry - Auth через Bearer JWT (декодер как в других сервисах) + service-token
Outbox-worker
-
PkOutboxWorker—@Scheduledраз в 3 сек - Пачками по 20 записей со
status=pending AND next_attempt_at <= now()FOR UPDATE SKIP LOCKED - Вызов PK по
op_type: create_invoice / reverse_payment / capture / repeatcnt / post_sale_receipt - При успехе
status=done, sent_at=now() - При ошибке — backoff (exp: 10s, 30s, 2m, 10m, 1h, 6h, 24h),
attempts++, max 10 →status=dead_letter - DLQ notification через Kafka на Admin BFF
Kafka consumers
-
OrderPaymentRequestedConsumer(grouppaykeeper-adapter-invoice) — insert вpk_outboxсop_type=create_invoice -
OrderRefundRequestedConsumer(grouppaykeeper-adapter-refund) — insert вpk_outboxсop_type=reverse_payment
Kafka producers
-
PkEventPublisherпубликует:paykeeper.invoice.created,paykeeper.payment.received,paykeeper.payment.refunded,paykeeper.receipt.fiscalized,paykeeper.receipt.failed,paykeeper.refund.failed,paykeeper.account.provisioned
Fiscal fetcher
-
FiscalFetcher— async послеpaykeeper.payment.received→GET /info/receipts/bypaymentid/→ сохранение вpaykeeper_receipts→ publishpaykeeper.receipt.fiscalized/paykeeper.receipt.failed
Per-invoice poll scheduler
-
InvoiceCheckScheduler— раз в 5 мин выбирает записиpk_invoice_check_scheduleсоstatus=active AND next_run_at <= now() - Вызов
GET /info/invoice/byid/→ еслиpaid— догрузка платежа + publishpaykeeper.payment.received(reconciled=true) - Удаление записи при paid / cancelled / expired
Ночной reconciliation cron
-
StuckPaymentsReconcileJob— cron0 0 3 * * ? - Per account:
GET /info/payments/bydate/?status[]=stuck+POST /change/payment/repeatcnt/ - Логирование в
pk_reconciliation_log
Retention / чистка
-
WebhookLogRetentionJob— ежесуточная чисткаwebhook_logстарше 90 дней -
OutboxRetentionJob— чисткаpk_outboxstatus=doneстарше 30 дней
Тесты
- Unit-тесты:
SignatureValidator(MD5 с Koala-vectors),OrderidPacker, маппинги PK polish→BigDecimal - Integration-тесты: WireMock для PK, Testcontainers для Postgres/Kafka
- E2E: полный цикл invoice → informer → receipt через fake PK
Деплой
- Docker image в registry
- Env на VPS:
PAYKEEPER_SECRETS_KEY(AES ключ),WEBHOOK_BASE_URL, DB creds - Docker-compose добавить сервис
- Nginx route
/pk-webhooks/*→:3015 - Liquibase migrations применить
- Kafka topics создать (7 топиков)
Критерии приёмки
- Создание PK-аккаунта через API возвращает 3 webhook URL
- Тестовый invoice создаётся в sandbox PK и возвращает
invoice_url - Informer с валидной MD5-подписью →
paykeeper.payment.receivedпубликуется, webhook отвечаетOK <md5> - Повторный informer с тем же
pk_payment_idне публикует дубль, отвечаетOK <md5> - Фискальные атрибуты подтягиваются через 30 сек после оплаты
- Refund через
POST /change/payment/reverse/→ через N минут приходит refund webhook →RefundRecord.status=done - При искусственно отключённом informer — заказ закрывается через per-invoice poll через 25-30 мин
- Ночной cron находит stuck-платежи и вызывает
repeatcnt