Манитон Docs

Платежи и СБП

Интеграция с банковской системой РФ

Платежи и СБП

Система быстрых платежей (СБП)

Интеграция с СБП — основной канал ввода/вывода ликвидности для розничных клиентов.

Пользовательские сценарии

  1. C2B (Пополнение кошелька):

    • Клиент выбирает сумму в приложении.
    • Получает DeepLink на банковское приложение.
    • Подтверждает платеж в банке.
    • Деньги зачисляются мгновенно (SLA < 15 сек).
  2. C2B (Оплата покупок):

    • Клиент сканирует QR-код на кассе магазина приложением "Манитон".
    • Система списывает CFA-RUB.
    • Система отправляет рубли мерчанту через банк-агент.

Диаграмма потока (QR-платеж)

Loading diagram...

Сверки и казначейство

Для обеспечения финансовой устойчивости используется модель ежедневного сведения (Daily Reconcile):

  • End-of-Day Check: В 23:59:59 остаток на номинальном счете в банке сверяется с объемом выпущенных токенов TotalSupply(CFA-RUB).
  • Расхождения: При обнаружении разницы (например, платеж прошел в банке, но не дошел вебхук) запускается процедура ручного разбора (Manual Adjustment), которая создает корректирующую проводку.

2) API (gRPC Connect)

CreatePaymentIntent

Создание намерения платежа для пополнения баланса.

message CreatePaymentIntentRequest {
  RequestContext context = 1;
  string user_id = 2;
  Money amount = 3;
  PaymentMethod method = 4;
}

message CreatePaymentIntentResponse {
  PaymentIntent intent = 1;
}

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;
}

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;
}

3) 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

Сверка балансов.

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 };
}

4) Мониторинг

Метрики

export const paymentMetrics = {
  paymentIntentsCreatedTotal: new Counter({
    name: 'payment_intents_created_total',
    help: 'Total number of payment intents created',
    labelNames: ['method'],
  }),

  paymentWebhooksReceivedTotal: new Counter({
    name: 'payment_webhooks_received_total',
    help: 'Total number of payment webhooks received',
    labelNames: ['status'],
  }),

  paymentProcessingDuration: new Histogram({
    name: 'payment_processing_duration_seconds',
    help: 'Time spent processing payments',
    labelNames: ['method'],
    buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 60],
  }),

  reconciliationDuration: new Histogram({
    name: 'reconciliation_duration_seconds',
    help: 'Time spent on reconciliation',
    buckets: [1, 5, 10, 30, 60],
  }),
};

5) 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