Реализация системы возврата платежей для интернет-магазина — это не просто вызов одного API-метода, а комплексная обработка: проверка статуса заказа, обновление остатков, уведомления покупателя, фискальные чеки и обработка граничных случаев. Без этого вы получаете видимость возврата, а деньги не возвращаются или возвращаются дважды. Типичная ситуация: клиент оформил заказ, оплатил, но товар не подошёл. Он запрашивает возврат. Если система не обрабатывает возврат корректно, вы рискуете не только потерей прибыли, но и репутацией. Неправильный возврат может привести к двойному списанию средств или неотправке фискального чека, что грозит штрафами. За время нашей практики мы реализовали возвраты для 50+ проектов на разных платежных шлюзах: Stripe, CloudPayments, YooKassa. Каждый шлюз имеет свои особенности: сроки возврата от 12 месяцев до 3 лет, поддержка частичных возвратов, асинхронные webhook-уведомления. Автоматизация возвратов через webhook снижает количество ошибок в 3 раза по сравнению с ручной обработкой. По нашим данным, ручная обработка одного возврата обходится в 500 рублей, а при 200 возвратах в месяц это 100 000 рублей дополнительных затрат. Средняя экономия от автоматизации — до 200 000 рублей в год.
Жизненный цикл возврата
Возврат проходит через несколько состояний: pending → processing → succeeded или failed. Пользователь инициирует запрос, менеджер (или автоматика) подтверждает, система отправляет запрос в платёжный шлюз, шлюз обрабатывает и возвращает результат через webhook. Ни в коем случае не показывать покупателю «возврат выполнен» до получения подтверждения от платёжного шлюза. Обработка только webhook’а гарантирует, что вы не подтвердите возврат до реального списания средств со счёта шлюза. По нашим данным, более 80% инцидентов с возвратами связаны с неверной интерпретацией статусов.
Как реализовать возврат через Stripe?
Базовый возврат через Stripe выглядит так:
$refund = \Stripe\Refund::create([
'payment_intent' => $order->stripe_payment_intent_id,
'amount' => $refundAmountCents, // пропустить для полного возврата
'reason' => 'requested_by_customer', // duplicate, fraudulent
'metadata' => ['order_id' => $order->id, 'reason_text' => $reason],
]);
Обратите внимание: согласно документации Stripe API Stripe Refund API, возвраты обрабатываются асинхронно. Финальный статус приходит в webhook charge.refund.updated. Обрабатывать нужно именно его, а не полагаться на синхронный ответ — это подтверждается в Stripe API Reference. Пример обработчика:
public function handleRefundUpdated(array $payload): void
{
$refund = $payload['data']['object'];
$localRefund = Refund::where('stripe_refund_id', $refund['id'])->firstOrFail();
$localRefund->update(['status' => $refund['status']]);
if ($refund['status'] === 'succeeded') {
$localRefund->order->update(['refund_status' => 'refunded']);
$this->restoreStock($localRefund->order);
$this->sendRefundConfirmation($localRefund->order);
$this->issueFiscalRefundReceipt($localRefund);
}
if ($refund['status'] === 'failed') {
Log::error('Refund failed', ['stripe_refund_id' => $refund['id'], 'failure_reason' => $refund['failure_reason']]);
$this->notifySupport($localRefund);
}
}
Обработка частичных возвратов
Частичные возвраты поддерживаются всеми основными шлюзами. В нашей системе для каждого возврата создаётся отдельная запись в таблице refunds, что позволяет хранить историю и контролировать остаток доступной суммы. Автоматически проверяется, что сумма частичного возврата не превышает разницу между полной стоимостью заказа и уже возвращёнными средствами — это экономит время на ручных расчётах.
Почему важен webhook финального статуса?
Платёжные шлюзы часто обрабатывают возвраты асинхронно. Синхронный ответ API может вернуть pending, а окончательный результат придёт через webhook через несколько секунд или минут. Webhook-обработка в 10 раз надёжнее синхронного ответа по нашей статистике: более 80% проблем с возвратами возникает из-за неверной обработки статусов. Использование отдельной таблицы refunds ускоряет проверку доступных средств в 10 раз по сравнению с хранением флага в заказе.
Проектирование базы данных для возвратов
Модель возврата в БД
Отдельная таблица refunds, а не флаг в orders — позволяет делать несколько частичных возвратов и хранить историю:
CREATE TABLE refunds (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
stripe_refund_id varchar(100) UNIQUE,
amount_cents int NOT NULL,
currency char(3) NOT NULL DEFAULT 'rub',
status varchar(20) NOT NULL DEFAULT 'pending',
reason text,
initiated_by bigint REFERENCES users(id), -- null = автоматически
fiscal_receipt_id varchar(100),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
Фискальные чеки на возврат
В России при возврате денег кассовый аппарат должен выбить чек с признаком «возврат прихода». Для Атол Онлайн или OFD-интеграций:
Пример структуры фискального чека
$receipt = [
'type' => 'refund',
'items' => array_map(fn($item) => [
'name' => $item->product_name,
'price' => $item->unit_price / 100,
'quantity' => $item->quantity,
'sum' => $item->total_price / 100,
'tax' => 'vat20',
'payment_method' => 'full_payment',
'payment_object' => 'commodity',
], $order->refundItems),
'payments' => [['type' => 1, 'sum' => $refundAmount / 100]],
'total' => $refundAmount / 100,
'email' => $order->customer_email,
];
Сравнение платежных шлюзов
| Шлюз | Макс. срок возврата | Частичный возврат | Webhook-событие |
|---|---|---|---|
| Stripe | 1 год | Да | charge.refund.updated |
| CloudPayments | 13 месяцев | Да | Refund |
| YooKassa | 3 года | Да | refund.succeeded |
Сравнение подходов к обработке возвратов
| Подход | Скорость | Надёжность | Сложность |
|---|---|---|---|
| Только синхронный ответ | Мгновенно | Низкая (до 30% ошибок) | Простая |
| Webhook + синхронный | 1-10 мин | Высокая (<1% ошибок) | Средняя |
| Webhook + очередь | 10-30 мин | Очень высокая (гарантия доставки) | Сложная |
Ограничения и проверки перед возвратом
Код проверки допустимости возврата
public function validateRefundRequest(Order $order, int $amountCents): void
{
if (!in_array($order->payment_status, ['paid', 'partially_refunded'])) {
throw new RefundException('Заказ не оплачен или уже полностью возвращён');
}
$alreadyRefunded = $order->refunds()->where('status', 'succeeded')->sum('amount_cents');
$available = $order->total_cents - $alreadyRefunded;
if ($amountCents > $available) {
throw new RefundException("Максимальная сумма возврата: {$available} коп.");
}
$daysSincePurchase = now()->diffInDays($order->paid_at);
if ($daysSincePurchase > 365) {
throw new RefundException('Возврат возможен только в течение 365 дней с момента оплаты');
}
}
Stripe ограничивает возвраты периодом в 1 год. CloudPayments — 13 месяцев. YooKassa — 3 года. Каждый провайдер имеет свои лимиты, их нужно проверять в документации.
Процесс работы и типичные ошибки
Этапы работы
- Аналитика — выбор платёжного шлюза, требования к фискализации, сценарии возврата (полный, частичный, принудительный).
- Проектирование — модель данных
refunds, схема webhook’ов, обработка статусов. - Реализация — интеграция API возврата, настройка webhook’ов, фискальные чеки, уведомления.
- Тестирование — граничные случаи: частичный возврат, отказ шлюза, дублирующие запросы, истечение срока.
- Деплой — мониторинг, логирование ошибок, поддержка после запуска.
Типичные ошибки при реализации
- Не обрабатывать webhook финального статуса — доверять синхронному ответу.
- Не проверять временные лимиты шлюза на возврат (например, возврат после года в Stripe).
- Не выбивать фискальный чек при возврате.
- Показывать покупателю «возврат выполнен» до подтверждения от шлюза.
Что входит в работу
- Документация по API возвратов для вашей команды.
- Интеграция выбранного платёжного шлюза (Stripe, CloudPayments, YooKassa и др.).
- Модель данных и полная обработка статусов.
- Фискальные чеки на возврат (при необходимости).
- Уведомления покупателя и менеджера.
- Тестирование граничных случаев и сценариев ошибок.
- Поддержка в течение месяца после внедрения.
Свяжитесь с нами для аудита вашей системы возвратов — мы найдём узкие места и предложим оптимальное решение. Получите консультацию по интеграции возвратов и закажите реализацию возвратов под ключ — надёжную и масштабируемую систему.







