Реалізація повернення платежу на веб-сайті
Повернення платежу — це не просто вклик одного API-методу. Коректна реалізація включає перевірку стану замовлення, оновлення залишків, сповіщення, фіскальні чеки і обробку граничних випадків. Без цього отримуємо видимість повернення, а гроші або не повертаються, або повертаються двічі.
На реалізацію йде 2–4 робочих дня залежно від кількості платіжних методів і вимог до фіскалізації.
Життєвий цикл повернення
Повернення проходить через кілька станів: pending → processing → succeeded або failed. Користувач ініціює запит, менеджер (або автоматика) підтверджує, система відправляє запит у платіжний шлюз, шлюз обробляє і повертає результат через webhook.
Ніколи не показувати покупцю «повернення виконано» до отримання підтвердження від платіжного шлюзу.
Stripe Refund API
Базове повернення через 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 обробляє повернення асинхронно. Фінальний статус приходить у webhook charge.refund.updated. Обрабатувати потрібно саме його, а не покладатися на синхронну відповідь:
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);
}
}
Модель повернення в БД
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()
);
Окрема таблиця refunds, а не флаг в orders — дозволяє робити кілька часткових повернень і зберігати історію.
Обмеження та перевірки перед поверненням
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 роки. Кожен провайдер має свої ліміти, їх потрібно перевіряти в документації.
Фіскальний чек на повернення
У Росії при повертанні грошей касовий апарат повинен вибити чек з ознакою «повернення прибутку». Для Атол Онлайн / 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,
];
Чек на повернення повинен видаватися незалежно від того, ініційовано чи повернення зі сторони клієнта або по ініціативі магазину.
Повернення для методів без карти
Для готівки, банківського переводу, СБП — автоматичний API-повернення неможливий. В цих випадках реалізується сценарій з ручною обробкою: менеджер підтверджує факт повернення, система оновлює статус. Для COD — часто означає відправку готівки кур'єром або банківський переводу — це повинно бути задокументовано в інтерфейсі.







