Манитон Docs

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
}

Бизнес-процесс пополнения

  1. Пользователь создаёт заявку на пополнение через API Gateway.
  2. Payments-RU генерирует QR-код или ссылку (СБП C2B).
  3. Пользователь оплачивает в своем банк-клиенте.
  4. Банк отправляет Webhook в Payments-RU.
  5. Payments-RU проверяет подпись и публикует событие FiatDepositedEvent в Kafka.
  6. 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;
}

Процесс:

  1. Валидация подписи вебхука
  2. Проверка суммы платежа
  3. Публикация события 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

Решение:

  1. Проверьте, что endpoint доступен извне
  2. Проверьте конфигурацию CORS
  3. Проверьте логи безопасности (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

Решение:

  1. Проверьте синхронизацию времени
  2. Проверьте транзакции, которые не прошли
  3. Ручной разбор расхождений

Дополнительные ресурсы

On this page