Розробники часто плутають наступні поняття, використовуючи їх як синоніми. Це призводить до помилок у логіці: наприклад, спроба «зробити refund» на авторизований, але ще не списаний платіж, коли правильна дія — Void.
Void — відміна зарезервованих (авторизованих) коштів до того, як відбувся capture. Кошти не рухаються — знімається лише бронь.
Authorized (до settlement)action = "cancel" для hold_wait платежівRefund — повернення вже розрахованих коштів покупцю. Це нова транзакція у зворотньому напрямку.
Full Refund) або частковим (Partial Refund)Chargeback — примусове повернення коштів, ініційоване банком-емітентом за заявою покупця. Відбувається поза вашим контролем.
Банк-емітент має право на chargeback якщо:
Для продавця chargeback несе подвійні втрати: кошти повертаються покупцю + штраф PSP ($15–50 за кожен chargeback).
Dispute — загальний термін для оскарження транзакції. Chargeback — один із видів dispute. У Stripe ecosystem «dispute» — це весь lifecycle процесу оскарження.
| Void | Refund | Chargeback | |
|---|---|---|---|
| Ініціатор | Продавець | Продавець | Покупець → банк-емітент |
| Час | Миттєво | 1–5 днів | 3–6 тижнів |
| Умова | До capture/settlement | Після settlement | Будь-коли (до 120 днів) |
| Штраф | Немає | Немає | $15–50 від PSP |
| Комісія back? | Так | Ні | Ні + штраф |
PaymentServicepublic async Task<RefundResult> InitiateRefundAsync(
Guid paymentId,
decimal? amount = null, // null = повне повернення
string? reason = null,
CancellationToken ct = default)
{
var payment = await _db.Payments
.FirstOrDefaultAsync(p => p.Id == paymentId, ct)
?? throw new NotFoundException($"Payment {paymentId} not found");
// Перевіряємо, що повернення взагалі можливе
if (payment.Status != PaymentStatus.Settled
&& payment.Status != PaymentStatus.PartiallyRefunded)
{
throw new InvalidOperationException(
$"Cannot refund payment in status {payment.Status}. " +
$"Only Settled or PartiallyRefunded payments can be refunded.");
}
// Визначаємо суму повернення
var refundAmount = amount ?? payment.Amount;
if (refundAmount <= 0 || refundAmount > payment.Amount)
throw new ArgumentException("Invalid refund amount");
// Ідемпотентний ключ для refund
var refundKey = $"refund_{paymentId}_{refundAmount}_{DateTimeOffset.UtcNow:yyyyMMddHH}";
// Делегуємо провайдеру
var provider = _providerFactory.GetProvider(payment.Provider);
var result = await provider.RefundAsync(
payment.ProviderTransactionId!, refundAmount, ct);
if (!result.Success)
{
throw new PaymentRefundException($"Refund failed: {result.ErrorMessage}");
}
// Оновлюємо статус платежу
var isFullRefund = refundAmount == payment.Amount;
payment.Status = isFullRefund
? PaymentStatus.Refunded
: PaymentStatus.PartiallyRefunded;
payment.UpdatedAt = DateTimeOffset.UtcNow;
// Зберігаємо запис про повернення
_db.Refunds.Add(new Refund
{
Id = Guid.NewGuid(),
PaymentId = paymentId,
Amount = refundAmount,
ProviderRefundId = result.RefundId,
Reason = reason,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync(ct);
return result;
}
public class Refund
{
public Guid Id { get; set; }
public Guid PaymentId { get; set; }
public Payment Payment { get; set; } = null!;
public decimal Amount { get; set; }
public string? ProviderRefundId { get; set; } // ID повернення від PSP
public string? Reason { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
group.MapPost("/{id}/refund", RefundPayment)
.WithName("RefundPayment")
.WithSummary("Ініціювати повернення коштів");
private static async Task<IResult> RefundPayment(
Guid id,
[FromBody] RefundRequest? request,
PaymentService paymentService,
CancellationToken ct)
{
var result = await paymentService.InitiateRefundAsync(
paymentId: id,
amount: request?.Amount,
reason: request?.Reason,
ct: ct);
return Results.Ok(new
{
RefundId = result.RefundId,
Message = result.Success ? "Повернення ініційовано" : result.ErrorMessage
});
}
public record RefundRequest(decimal? Amount, string? Reason);
Коли покупець ініціює chargeback, PSP надсилає вам повідомлення (через webhook або email). У вас є обмежений час (зазвичай 7–21 день) для подачі «доказів» (evidence).
Докази для оскарження chargeback:
Найкращий refund — той, якого не довелося робити. Запобігіть шахрайським транзакціям:
3-D Secure 2.x
Velocity checks
AVS (Address Verification System)
Чорний список
Як розробник платіжного модуля, вам необхідно знати правову рамку:
Закон України «Про захист прав споживачів» (Закон № 1023-XII) надає споживачам право на:
Для digital-продуктів (SaaS, ліцензії):
Строки повернення коштів:
Правильна обробка повернень вимагає чіткого розуміння різниці між Void (до settlement), Refund (після settlement) та Chargeback (примусове, ініційоване покупцем). Реалізуйте refund через абстракцію IPaymentProvider, зберігайте всі повернення у таблиці Refunds для аудиту. Anti-fraud заходи (3DS, velocity checks) знижують необхідність у поверненнях та захищають від перевищення ліміту chargebacks.
Підписки та рекурентні платежі
Архітектура підписних платежів (subscriptions) — токенізація карток, Merchant-Initiated Transactions, billing cycle, dunning та реалізація через LiqPay і Stripe.
Тестування платіжних інтеграцій
Стратегії тестування платіжної підсистеми — Sandbox режими, тестові картки, unit/integration тести з mock-провайдерами, webhook тестування та CI/CD.