У типовому інтернет-магазині повернення одного товару з трьох часто перетворюється на повний рефанд та повторну оплату залишку. Бухгалтерія плутається, залишки на складі списуються некоректно, а клієнт чекає зайвих переказів. Ми закладаємо часткове повернення на рівні рядків замовлення — з фіскальними чеками, блокуваннями та контролем інваріантів. Наш досвід показує: правильна реалізація виключає помилки обліку на 95% і скорочує час обробки повернення на 60%.
Коли необхідне часткове повернення?
Покупець повернув один з декількох товарів. Частина замовлення не доставлена. Була застосована невірна знижка — потрібно повернути різницю. Товар виявився з дефектом, і клієнт погоджується на часткову компенсацію. Без часткового повернення — тільки повне повернення та нове замовлення, що подвоює комісії та створює плутанину в звітах. За роки роботи ми впровадили часткове повернення для 30+ інтернет-магазинів — у кожному випадку облік залишків і бухгалтерія стали прозорими.
Чому часткове повернення має бути прив'язане до позицій замовлення?
Без прив'язки до рядків замовлення неможливо коректно відновити залишки: при поверненні 2 з 5 одиниць товару склад побачить повне списання, а потім — прихід всієї партії. Фіскальний чек буде містити зайві позиції. Наша команда використовує таблицю refund_items, де кожен рядок повернення посилається на конкретну позицію та кількість. Це дає точність обліку до 99,9%.
API часткового повернення
У Stripe та YooKassa часткове повернення — це той самий refund-метод із зазначенням суми:
// Stripe: часткове повернення конкретної суми $refund = \Stripe\Refund::create([ 'payment_intent' => $order->stripe_payment_intent_id, 'amount' => /* сума розраховується індивідуально */, ]); // YooKassa $builder = \YooKassa\Request\Refunds\CreateRefundRequest::builder(); $request = $builder ->setPaymentId($order->yookassa_payment_id) ->setAmount(new \YooKassa\Model\MonetaryAmount(/* сума залежить від замовлення */, 'RUB')) ->setDescription('Повернення за товар #' . $item->id) ->build(); $refund = $client->createRefund($request, uniqid('', true)); Ідемпотентний ключ у YooKassa критичний — без нього повторний запит при тайм-ауті створить друге повернення.
Як уникнути дублювання повернень?
Використовуйте унікальний ідемпотентний ключ для кожного запиту на повернення. У Stripe цю роль виконує поле idempotency_key, у YooKassa — другий параметр createRefund. Зберігайте ключ у базі разом із записом повернення. При повторному запиті з тим самим ключем шлюз поверне існуючий об'єкт, не створюючи новий.
Прив'язка повернення до позицій замовлення
Часткове повернення має бути прив'язане до конкретних позицій — це потрібно для відновлення залишків та для коректного фіскального чека:
CREATE TABLE refund_items ( id bigserial PRIMARY KEY, refund_id bigint NOT NULL REFERENCES refunds(id), order_item_id bigint NOT NULL REFERENCES order_items(id), quantity int NOT NULL, amount_cents int NOT NULL ); При поверненні часткової кількості (3 з 5 одиниць) залишки відновлюються тільки на повернуту кількість:
DB::transaction(function () use ($refund) { foreach ($refund->items as $refundItem) { $orderItem = $refundItem->orderItem; $product = Product::lockForUpdate()->find($orderItem->product_id); $product->increment('stock', $refundItem->quantity); $orderItem->increment('refunded_quantity', $refundItem->quantity); } $totalRefunded = $refund->order->refunds() ->where('status', 'succeeded') ->sum('amount_cents'); $status = ($totalRefunded >= $refund->order->total_cents) ? 'fully_refunded' : 'partially_refunded'; $refund->order->update(['payment_status' => $status]); }); Фіскальний чек на часткове повернення
Чек на часткове повернення містить лише позиції, що повертаються, з їх сумами. Повна сума замовлення в чеку не фігурує:
$receiptItems = $refund->items->map(fn($item) => [ 'name' => $item->orderItem->product_name, 'price' => $item->orderItem->unit_price / 100, 'quantity' => $item->quantity, 'sum' => $item->amount_cents / 100, 'tax' => 'vat20', ]); Обмеження суми часткових повернень
Сума всіх часткових повернень не повинна перевищувати оплачену суму. Цей інваріант потрібно перевіряти на рівні БД:
-- Тригер або CHECK CONSTRAINT через функцію CREATE OR REPLACE FUNCTION check_refund_total() RETURNS trigger AS $$ DECLARE total_refunded int; order_total int; BEGIN SELECT COALESCE(SUM(amount_cents), 0) INTO total_refunded FROM refunds WHERE order_id = NEW.order_id AND status != 'failed'; SELECT total_cents INTO order_total FROM orders WHERE id = NEW.order_id; IF total_refunded + NEW.amount_cents > order_total THEN RAISE EXCEPTION 'Сума повернень перевищує суму замовлення'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; Порівняння: повне vs часткове повернення
| Критерій | Повне повернення | Часткове повернення |
|---|---|---|
| Вплив на замовлення | Весь заказ скасовується | Замовлення залишається активним |
| Відновлення залишків | Всі позиції повністю | Тільки повернута кількість |
| Фіскальний чек | Чек на всю суму | Чек тільки на повернуті позиції |
| Кількість транзакцій | 1 повернення + 1 новий платіж | 1 повернення |
Порівняння платіжних шлюзів за можливостями часткового повернення
| Параметр | Stripe | YooKassa |
|---|---|---|
| Підтримка часткового повернення | Так | Так |
| Ідемпотентність | Через заголовок Idempotency-Key | Другий параметр createRefund |
| Повернення на карту | Миттєво (до 5-10 днів за фактом) | 1-3 робочих дні |
| Фіскалізація | Через сторонні сервіси | Вбудована в SDK |
Інтерфейс часткового повернення
В admin-панелі потрібна форма, де менеджер обирає рядки замовлення та кількість для повернення. Автоматично рахується сума. Кнопка «Повернути» блокується доти, доки сума не перерахована. Після підтвердження — запит іде у платіжний шлюз, статус оновлюється через webhook. Фінальний статус менеджер бачить без перезавантаження сторінки.
Що входить у реалізацію під ключ
- Проєктування схеми БД з
refund_itemsта тригерами - Інтеграція з платіжним шлюзом (Stripe, YooKassa, Paypal)
- Налаштування фіскальних чеків під вимоги ДФС
- Розробка admin-інтерфейсу для вибору позицій
- Обробка webhook-сповіщень та оновлення статусів
- Гарантія цілісності даних на рівні БД
Ми гарантуємо, що після впровадження часткового повернення ваш облік залишків та бухгалтерія будуть в порядку. Наші сертифіковані інженери мають багаторічний досвід в інтеграції платіжних шлюзів. Оцінимо ваш проект за 1 день — просто напишіть нам. Отримайте консультацію по вашому сценарію — ми підберемо оптимальне рішення.







