Реализация частичного возврата платежа на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация частичного возврата платежа на сайте
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация частичного возврата платежа на сайте

Частичный возврат — отдельный сценарий, который часто реализуется небрежно: делают полный возврат и новый платёж, или просто выдают купон. Оба варианта создают проблемы с бухгалтерией и отчётностью. Правильная реализация частичного рефанда работает на уровне строк заказа.

Когда нужен частичный возврат

Покупатель вернул один из нескольких товаров. Часть заказа недоставлена. Была применена неверная скидка — нужно вернуть разницу. Товар оказался с дефектом и клиент согласен на частичную компенсацию.

API частичного возврата

У Stripe, YooKassa и большинства провайдеров частичный возврат — это тот же refund-метод с указанием суммы:

// Stripe: частичный возврат конкретной суммы
$refund = \Stripe\Refund::create([
    'payment_intent' => $order->stripe_payment_intent_id,
    'amount'         => 150000, // 1500.00 RUB в копейках
]);

// YooKassa
$builder = \YooKassa\Request\Refunds\CreateRefundRequest::builder();
$request = $builder
    ->setPaymentId($order->yookassa_payment_id)
    ->setAmount(new \YooKassa\Model\MonetaryAmount('1500.00', 'RUB'))
    ->setDescription('Возврат за товар #' . $item->id)
    ->build();
$refund = $client->createRefund($request, uniqid('', true));

Идемпотентный ключ в YooKassa критичен — без него повторный запрос при таймауте создаст второй возврат.

Привязка возврата к позициям заказа

Частичный возврат должен быть привязан к конкретным позициям — это нужно для восстановления остатков и для корректного фискального чека:

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;

Интерфейс частичного возврата

В admin-панели нужна форма, где менеджер выбирает строки заказа и количество для возврата. Автоматически считается сумма. Кнопка «Вернуть» блокируется до тех пор, пока сумма не пересчитана. После подтверждения — запрос уходит в платёжный шлюз, статус обновляется через webhook. Финальный статус менеджер видит без перезагрузки страницы.