Реализация холдирования средств на сайте
Холдирование (авторизация без списания) — механизм, при котором деньги блокируются на карте покупателя, но не поступают продавцу до явного подтверждения. Используется там, где окончательная сумма или факт выполнения заказа неизвестны в момент оплаты: доставка с оплатой по факту, бронирование с переменной стоимостью, предзаказ.
Как работает двухстадийная оплата
Процесс состоит из двух операций: authorize (авторизация, блокировка средств) и capture (захват, фактическое списание). Между ними — произвольный промежуток времени, но с ограничением: большинство банков снимают холд через 7 дней, некоторые через 30. Если capture не произошёл — холд снимается автоматически, деньги возвращаются.
Stripe: authorize + capture
В Stripe двухстадийная оплата реализуется через capture_method: manual:
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $order->total_cents,
'currency' => 'eur',
'capture_method' => 'manual', // не списывать сразу
'payment_method_types' => ['card'],
'metadata' => ['order_id' => $order->id],
]);
После подтверждения 3DS покупателем PaymentIntent переходит в статус requires_capture. Деньги заблокированы, но не списаны. Когда заказ готов к выдаче:
\Stripe\PaymentIntent::capture($order->stripe_payment_intent_id, [
'amount_to_capture' => $finalAmountCents, // можно меньше изначального
]);
Capture можно выполнить на сумму, меньшую заблокированной — например, если часть товара оказалась недоступна. На большую — нельзя.
YooKassa: двухстадийная оплата
$payment = $client->createPayment([
'amount' => ['value' => '1500.00', 'currency' => 'RUB'],
'capture' => false, // двухстадийная оплата
'payment_method_data' => ['type' => 'bank_card'],
'confirmation' => ['type' => 'redirect', 'return_url' => $returnUrl],
'description' => 'Заказ #' . $order->id,
], uniqid('', true));
Когда готовы списать:
$client->capturePayment(
['amount' => ['value' => '1500.00', 'currency' => 'RUB']],
$order->yookassa_payment_id,
uniqid('', true)
);
Отмена холда (cancel authorization)
Если заказ отменён до capture — нужно явно отменить авторизацию, иначе деньги остаются заблокированными до истечения срока:
// Stripe
\Stripe\PaymentIntent::cancel($order->stripe_payment_intent_id, [
'cancellation_reason' => 'abandoned',
]);
// YooKassa
$client->cancelPayment($order->yookassa_payment_id, uniqid('', true));
Мониторинг истекающих холдов
Срок действия авторизации — критический параметр. Нужен фоновый процесс, который находит холды, близкие к истечению:
// Команда, запускаемая раз в час
$expiringSoon = Order::where('payment_status', 'authorized')
->where('authorized_at', '<', now()->subDays(6)) // Stripe даёт 7 дней
->get();
foreach ($expiringSoon as $order) {
// Уведомить менеджера или автоматически отменить
Notification::send($order->manager, new AuthorizationExpiring($order));
}
Хранить authorized_at в таблице заказов — обязательно. Срок Stripe — 7 дней для большинства карт, до 31 дня для некоторых. Лучше ориентироваться на 5–6 дней как безопасный лимит.
Partial capture
Stripe поддерживает partial capture — захват суммы меньше авторизованной. Разница автоматически разблокируется. Это полезно для сценариев с переменной стоимостью доставки: сначала авторизуем максимум, по факту списываем реальную стоимость.
\Stripe\PaymentIntent::capture($intentId, [
'amount_to_capture' => $actualAmountCents,
]);
// Остаток ($authorizedAmount - $actualAmount) автоматически разблокируется
CloudPayments и большинство российских провайдеров partial capture тоже поддерживают, но синтаксис и поведение нужно проверять в документации конкретного шлюза — различия есть.







