Payments-RU Service
Шлюз для работы с российскими платежными системами
Payments-RU Service (В разработке)
Статус: Проектирование. Шлюз для работы с российскими платежными системами.
Payments-RU обеспечивает мост между традиционной банковской системой РФ и платформой Манитон.
Основные функции
- Интеграция с СБП (Система Быстрых Платежей): Генерация QR-кодов и ссылок для оплаты.
- Обработка вебхуков: Прием уведомлений от банков об успешных платежах.
- Вывод средств: Инициация обратных платежей пользователям.
- Сверка (Reconciliation): Ежедневная проверка соответствия остатков на банковском счету и объема выпущенных CFA-RUB.
Технологический стек
- Фреймворк: NestJS
- База данных: PostgreSQL (для логов платежей и идемпотентности)
- Интеграции: API Банков-партнеров, НСПК.
Модель данных (Protobuf)
message PaymentIntent {
string payment_id = 1;
string user_id = 2;
Money amount = 3;
PaymentMethod method = 4; // SBP_C2B / SBP_QR / BANK_TRANSFER
PaymentStatus status = 5;
string payment_url = 6;
string qr_payload = 7;
}
message PaymentWebhook {
string payment_id = 1;
PaymentStatus status = 2;
Money amount = 3;
string provider_payload = 4;
}
message PayoutRequest {
string payout_id = 1;
string user_id = 2;
Money amount = 3;
string bank_account = 4;
PaymentStatus status = 5;
}
message ReconcileReport {
string report_id = 1;
Timestamp business_date = 2;
Money bank_balance = 3; // Баланс на счёте банка
Money token_supply = 4; // Общий supply CFA-RUB
bool balanced = 5; // bank_balance == token_supply
}Бизнес-процесс пополнения
- Пользователь создаёт заявку на пополнение через API Gateway.
- Payments-RU генерирует QR-код или ссылку (СБП C2B).
- Пользователь оплачивает в своем банк-клиенте.
- Банк отправляет Webhook в Payments-RU.
- Payments-RU проверяет подпись и публикует событие
FiatDepositedEventв Kafka. - CFA-Core получает событие и выпускает (mint) соответствующие
CFA-RUBв DLT.
Безопасность
- Идемпотентность: Строгая проверка уникальности каждого входящего платежа.
- Подпись запросов: Использование криптографических ключей для связи с банками.
- Тайм-ауты: Настроенные политики ретраев для внешних вызовов.
API (gRPC Connect)
PaymentsService
CreatePaymentIntent
Создание намерения платежа для пополнения баланса.
message CreatePaymentIntentRequest {
RequestContext context = 1;
string user_id = 2;
Money amount = 3;
PaymentMethod method = 4;
}
message CreatePaymentIntentResponse {
PaymentIntent intent = 1;
}Пример:
const payment = await paymentsService.createPaymentIntent(
{ requestId, correlationId, idempotencyKey },
'user-123',
{ amount: '10000', currencyCode: 'RUB' },
PaymentMethod.SBP_QR,
);HandlePaymentWebhook
Обработка вебхука от банка о статусе платежа.
message HandlePaymentWebhookRequest {
RequestContext context = 1;
PaymentWebhook webhook = 2;
}
message PaymentWebhook {
string payment_id = 1;
PaymentStatus status = 2;
Money amount = 3;
string provider_payload = 4;
google.protobuf.Timestamp received_at = 5;
}Процесс:
- Валидация подписи вебхука
- Проверка суммы платежа
- Публикация события
FiatDepositedEventв Kafka
InitiatePayout
Инициация вывода средств пользователю.
message InitiatePayoutRequest {
RequestContext context = 1;
PayoutRequest payout = 2;
}
message PayoutRequest {
string payout_id = 1;
string user_id = 2;
Money amount = 3;
string bank_account = 4;
}Reconcile
Сверка балансов банковского счета и выпущенных CFA-RUB.
message ReconcileRequest {
RequestContext context = 1;
google.protobuf.Timestamp business_date = 2;
}
message ReconcileResponse {
ReconcileReport report = 1;
}
message ReconcileReport {
string report_id = 1;
Money bank_balance = 2;
Money token_supply = 3;
bool balanced = 4;
string notes = 5;
}События Kafka
Исходящие (maniton.payments.events.v1)
FiatDepositedEvent
message FiatDepositedEvent {
RequestContext context = 1;
string user_id = 2;
Money amount = 3;
string payment_id = 4;
google.protobuf.Timestamp deposited_at = 5;
}PayoutConfirmedEvent
message PayoutConfirmedEvent {
RequestContext context = 1;
string user_id = 2;
Money amount = 3;
string payout_id = 4;
google.protobuf.Timestamp confirmed_at = 5;
}Интеграции
СБП (Система быстрых платежей)
API:
interface SbpClient {
createQR(amount: number, purpose: string): Promise<SbpQR>;
getPaymentStatus(paymentId: string): Promise<SbpPaymentStatus>;
getBalance(): Promise<SbpBalance>;
}
interface SbpQR {
qrPayload: string;
paymentUrl: string;
expiresAt: Date;
}
interface SbpPaymentStatus {
paymentId: string;
status: 'PENDING' | 'SUCCESS' | 'FAILED' | 'EXPIRED';
amount: number;
transactionId?: string;
}Процесс пополнения:
Loading diagram...
Банковские API
API:
interface BankClient {
initiateTransfer(details: TransferDetails): Promise<TransferResult>;
getTransferStatus(transferId: string): Promise<TransferStatus>;
getBalance(): Promise<BankBalance>;
}
interface TransferDetails {
accountNumber: string;
recipientAccount: string;
amount: number;
purpose: string;
}Процесс вывода:
Loading diagram...
Use Cases
CreatePaymentIntentUseCase
Создание намерения платежа и генерация QR-кода.
async execute(request: CreatePaymentIntentRequest): Promise<CreatePaymentIntentResponse> {
const { context, userId, amount, method } = request;
// 1. Проверка лимитов
const limitsCheck = await this.identityClient.checkLimits(
context,
userId,
LimitType.DEPOSIT,
amount,
OperationType.DEPOSIT
);
if (!limitsCheck.allowed) {
throw new Error(limitsCheck.reason);
}
// 2. Создание PaymentIntent
const paymentIntent = new PaymentIntent(
crypto.randomUUID(),
userId,
amount,
method,
PaymentStatus.PENDING
);
// 3. Генерация QR-кода
if (method === PaymentMethod.SBP_QR) {
const qr = await this.sbpClient.createQR(
Number(amount.amount.value),
'Пополнение баланса'
);
paymentIntent.qrPayload = qr.qrPayload;
paymentIntent.paymentUrl = qr.paymentUrl;
}
// 4. Сохранение
await this.paymentIntentRepository.save(paymentIntent);
// 5. Публикация события
await this.eventPublisher.publish({
type: 'PaymentIntentCreated',
payload: paymentIntent,
});
return { intent: paymentIntent };
}HandlePaymentWebhookUseCase
Обработка вебхука от банка.
async execute(request: HandlePaymentWebhookRequest): Promise<HandlePaymentWebhookResponse> {
const { context, webhook } = request;
// 1. Валидация подписи
if (!this.validateSignature(webhook)) {
throw new Error('Invalid signature');
}
// 2. Проверка суммы
const paymentIntent = await this.paymentIntentRepository.findById(webhook.paymentId);
if (!paymentIntent) {
throw new Error('Payment intent not found');
}
if (webhook.amount.amount.value !== paymentIntent.amount.amount.value) {
throw new Error('Amount mismatch');
}
// 3. Обновление статуса
paymentIntent.status = webhook.status;
await this.paymentIntentRepository.save(paymentIntent);
// 4. Публикация события
if (webhook.status === PaymentStatus.SUCCESS) {
await this.eventPublisher.publish({
type: 'FiatDeposited',
payload: {
userId: paymentIntent.userId,
amount: paymentIntent.amount,
paymentId: paymentIntent.paymentId,
},
});
}
return { intent: paymentIntent };
}ReconcileUseCase
Сверка балансов банковского счета и выпущенных CFA-RUB.
async execute(request: ReconcileRequest): Promise<ReconcileResponse> {
const { context, businessDate } = request;
// 1. Получение баланса банка
const bankBalance = await this.bankClient.getBalance();
// 2. Получение supply CFA-RUB
const cfaRubSupply = await this.cfaClient.getSupply('CFA-RUB');
// 3. Сравнение
const balanced = bankBalance.amount.value === cfaRubSupply.amount.value;
// 4. Создание отчета
const report = {
reportId: crypto.randomUUID(),
businessDate,
bankBalance,
tokenSupply: cfaRubSupply,
balanced,
notes: balanced ? 'Balanced' : 'Discrepancy detected',
};
// 5. Сохранение отчета
await this.reconciliationReportRepository.save(report);
// 6. Алерт если не совпадает
if (!balanced) {
await this.alertService.sendAlert({
severity: 'critical',
message: 'Reconciliation failed',
details: report,
});
}
return { report };
}Мониторинг
Метрики
import { Counter, Histogram, Gauge } from 'prom-client';
export const paymentIntentsCreatedTotal = new Counter({
name: 'payment_intents_created_total',
help: 'Total number of payment intents created',
labelNames: ['method'],
});
export const paymentWebhooksReceivedTotal = new Counter({
name: 'payment_webhooks_received_total',
help: 'Total number of payment webhooks received',
labelNames: ['status'],
});
export const paymentProcessingDuration = new Histogram({
name: 'payment_processing_duration_seconds',
help: 'Time spent processing payments',
labelNames: ['method'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
export const reconciliationDuration = new Histogram({
name: 'reconciliation_duration_seconds',
help: 'Time spent on reconciliation',
buckets: [1, 5, 10, 30, 60],
});Логи
this.logger.log('Payment intent created', {
paymentId: paymentIntent.paymentId,
userId: paymentIntent.userId,
amount: paymentIntent.amount,
method: paymentIntent.method,
});
this.logger.log('Payment webhook received', {
paymentId: webhook.paymentId,
status: webhook.status,
amount: webhook.amount,
});
this.logger.log('Reconciliation completed', {
reportId: report.reportId,
balanced: report.balanced,
bankBalance: report.bankBalance,
tokenSupply: report.tokenSupply,
});Troubleshooting
Проблема: Вебхук не приходит
Диагностика:
# Проверка логов сервиса
kubectl logs -n maniton payments-service
# Проверка доступности endpoint
curl -X POST https://payments-service/webhook \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
# Проверка конфигурации вебхука
kubectl get configmap payments-service -o yamlРешение:
- Проверьте, что endpoint доступен извне
- Проверьте конфигурацию CORS
- Проверьте логи безопасности (WAF)
Проблема: Сверка не проходит
Диагностика:
# Проверка баланса банка
curl https://bank-api.example.com/balance
# Проверка supply CFA-RUB
curl https://cfa-core.example.com/supply?instrument=CFA-RUB
# Проверка отчета сверки
curl https://payments-service.example.com/reconciliation/latestРешение:
- Проверьте синхронизацию времени
- Проверьте транзакции, которые не прошли
- Ручной разбор расхождений