Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Возврат средств: практика refund в платёжной интеграции

Возврат средств: частичный refund и его учёт

Возврат платежа: частичный и полный refund, идемпотентность запроса, сроки зачисления, отмена возврата и сверка с отчётом провайдера.

Прямой ответ

Возврат средств — отдельная операция, а не отмена платежа. Провайдер создаёт запись возврата со своим идентификатором, суммой и статусом, поэтому возврат учитывается как самостоятельная сущность.

import httpx

r = httpx.post(
    f"",
    auth=(SHOP_ID, SECRET_KEY),
    headers={"Idempotency-Key": f"refund-{order_id}-{refund_no}"},
    json={"amount": {"value": "500.00", "currency": "RUB"}, "reason": "partial"},
    timeout=10.0,
)

Частичный возврат указывает сумму, полный — либо сумму целиком, либо признак полного возврата в зависимости от API провайдера.

Тип возврата Сумма Повторный возврат
Полный Равна сумме платежа Не выполняется
Частичный Меньше суммы платежа Возможны несколько
Повторный частичный В пределах остатка До исчерпания суммы

Ограничение определяется остатком: сумма всех возвратов не превышает сумму платежа. Проверка остатка выполняется на стороне сервиса, поскольку запрос сверх остатка отклоняется провайдером уже после принятия решения о возврате.

CREATE TABLE refunds (
    id            bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payment_id    bigint NOT NULL REFERENCES payments(id),
    provider_refund_id text,
    amount_minor  bigint NOT NULL,
    status        text NOT NULL,
    created_at    timestamptz NOT NULL DEFAULT now()
);

SELECT amount_minor - COALESCE(SUM(amount_minor), 0) AS available
FROM payments p LEFT JOIN refunds r ON r.payment_id = p.id AND r.status <> 'failed'
WHERE p.id = $1
GROUP BY p.amount_minor;

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

Статус возврата и момент фактического зачисления

Запрос возврата принимается провайдером немедленно, а зачисление средств выполняется позже. Между принятием и зачислением существует промежуток, который влияет на учёт.

Принят, обрабатывается. Возврат зарегистрирован у провайдера, но средства не списаны с счёта продавца.

Выполнен. Средства отправлены в платёжную систему пользователя.

Отклонён. Возврат отклонён по причине на стороне банка либо платёжной системы.

Срок зачисления определяется платёжной системой и банком пользователя, поэтому сервис не управляет им. Практическое следствие: статус возврата в собственной базе обновляется по уведомлению либо запросу, а не по факту принятия запроса.

Отдельный вопрос — отмена возврата. Возможность отмены зависит от провайдера и момента: до завершения обработки возврат может быть отменён, после завершения — нет. Отмена выполняется отдельной операцией с собственным статусом.

Идемпотентность запроса возврата обязательна, поскольку повтор при таймауте приводит к двойному возврату; о формировании ключа — в статье про ключ идемпотентности. Ключ формируется из идентификатора возврата, а не из идентификатора платежа, иначе второй частичный возврат отклоняется как повтор первого.

Возврат и комиссия: что фактически возвращается

Сумма, которую получает пользователь, и сумма, которую теряет продавец, не всегда совпадают. Различие определяется политикой провайдера по комиссии.

Комиссия не возвращается. Часть провайдеров удерживает комиссию по операции независимо от возврата. Продавец теряет комиссию, а пользователь получает полную сумму платежа.

Комиссия возвращается частично. Некоторые провайдеры возвращают фиксированную часть комиссии, поэтому фактические потери меньше полной комиссии.

Комиссия возвращается полностью. Возврат включает комиссию, и потери продавца ограничиваются стоимостью обработки возврата, если она предусмотрена.

Практическое следствие: расчёт фактически возвращённой суммы выполняется по данным провайдера, а не вычисляется из суммы платежа. Учёт комиссии ведётся отдельно от суммы возврата, поскольку комиссия относится к расходам, а не к возврату средств.

Отдельный случай — возврат по операции, выполненной в другой валюте. Курс на момент возврата отличается от курса на момент платежа, поэтому сумма в валюте пользователя может отличаться от исходной. Разница не является ошибкой и объясняется изменением курса.

Учёт возвратов в отчётности требует разделения: сумма возврата относится к уменьшению выручки, а комиссия — к расходам. Смешение двух величин искажает расчёт фактической выручки за период.

Практика: организация возврата средств

1. Храните возвраты отдельной сущностью. Возврат имеет собственный идентификатор, сумму и статус, поэтому не сводится к изменению статуса платежа.

2. Проверяйте остаток перед запросом. Сумма возвратов не превышает сумму платежа, и проверка выполняется до отправки запроса.

3. Формируйте ключ идемпотентности на возврат. Ключ из идентификатора возврата позволяет выполнять несколько частичных возвратов по одному платежу.

4. Учитывайте сумму в минимальных единицах. Копейки как целое число исключают ошибку округления при подсчёте остатка.

5. Обновляйте статус по ответу провайдера. Принятие запроса не означает зачисление средств, поэтому статус уточняется уведомлением либо запросом.

6. Исключайте отклонённые возвраты из остатка. Возврат со статусом «отклонён» не уменьшает доступный остаток.

7. Фиксируйте причину возврата. Причина требуется для отчётности и разбора спорных ситуаций.

8. Сверяйте возвраты с отчётом провайдера. Расхождение между учётом сервиса и отчётом указывает на пропущенное обновление статуса; о сверке — в статье про платёжные отчёты.

Рекомендации:

  1. Не выполняйте возврат повторным созданием платежа. Возврат — отдельная операция с собственной семантикой и учётом.
  2. Ограничивайте автоматические возвраты. Автоматический возврат без проверки остатка и статуса платежа приводит к ошибкам.
  3. Учитывайте комиссию провайдера. Часть провайдеров не возвращает комиссию по операции, что влияет на расчёт фактически возвращённой суммы.
  4. Проверяйте идемпотентность в тестах. Повторный запрос с тем же ключом не должен создавать второй возврат.
  5. Документируйте сроки зачисления. Пользователь ожидает средства в срок, определяемый платёжной системой, а не сервисом.

Практический сценарий: двойной возврат при таймауте запроса

Контекст: пользователь получил двойное зачисление по одному частичному возврату.

  1. Диагностика. В отчёте провайдера два возврата с одинаковой суммой по одному платежу. В базе сервиса присутствовала одна запись возврата в статусе «обрабатывается».
  2. Причина. Запрос возврата выполнен без ключа идемпотентности. Ответ не дошёл из-за таймаута, поэтому повтор создал второй возврат. Запись в базе осталась одна, поскольку вторая вставка выполнялась с тем же внутренним идентификатором операции.
  3. Действия. (а) Добавили ключ идемпотентности из идентификатора возврата; (б) сохранили идентификатор возврата у провайдера до ожидания ответа, чтобы повтор находил существующую операцию; (в) при таймауте перевели возврат в неопределённое состояние с уточнением запросом; (г) добавили проверку суммы возвратов по платежу в периодическую сверку с отчётом провайдера; (д) инициировали возврат излишне зачисленной суммы через провайдера.
  4. Результат. Повтор запроса возврата возвращает существующую операцию, а расхождение с отчётом обнаруживается сверкой.

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

FAQ

Можно ли выполнить несколько частичных возвратов? Да, в пределах остатка суммы платежа. Каждый возврат требует отдельного ключа идемпотентности.

Когда средства фактически возвращаются пользователю? Срок определяется платёжной системой и банком пользователя. Сервис фиксирует статус возврата по ответу провайдера.

Можно ли отменить возврат? Зависит от провайдера и статуса операции. До завершения обработки отмена возможна, после завершения — нет.

Уменьшает ли отклонённый возврат доступный остаток? Нет. Возврат со статусом «отклонён» исключается из подсчёта остатка.

Источники: Stripe refunds, PayPal Orders API, PostgreSQL numeric functions — ссылки в шапке.