Do not crash when refunding an invoice that has been marked settled (Fix #6003) (#6086)

This commit is contained in:
Nicolas Dorier 2024-07-04 16:43:30 +09:00 committed by GitHub
parent 4a2f61de9f
commit 247532e3c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 19 deletions

View file

@ -2247,6 +2247,17 @@ namespace BTCPayServer.Tests
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(0.79m, pp.Amount);
// If an invoice doesn't have payment because it has been marked as paid, we should still be able to refund it.
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest { Status = InvoiceStatus.Settled });
var refund = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal(1.0m, refund.Amount);
Assert.Equal("BTC", refund.Currency);
}
[Fact(Timeout = TestTimeout)]

View file

@ -408,10 +408,7 @@ namespace BTCPayServer.Controllers.Greenfield
var supported = _payoutHandlers.GetSupportedPayoutMethods(store);
if (supported.Contains(payoutMethodId))
{
var paymentMethodId = PaymentMethodId.GetSimilarities([payoutMethodId], invoice.GetPayments(false).Select(p => p.PaymentMethodId))
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
var paymentMethodId = invoice.GetClosestPaymentMethodId([payoutMethodId]);
paymentPrompt = paymentMethodId is null ? null : invoice.GetPaymentPrompt(paymentMethodId);
}
}
@ -426,6 +423,14 @@ namespace BTCPayServer.Controllers.Greenfield
var accounting = paymentPrompt.Calculate();
var cryptoPaid = accounting.Paid;
var dueAmount = accounting.TotalDue;
// If no payment, but settled and marked, assume it has been fully paid
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatus.Settled, ExceptionStatus: InvoiceExceptionStatus.Marked })
{
cryptoPaid = accounting.TotalDue;
dueAmount = 0;
}
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
@ -491,8 +496,6 @@ namespace BTCPayServer.Controllers.Greenfield
{
return this.CreateValidationError(ModelState);
}
var dueAmount = accounting.TotalDue;
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;

View file

@ -302,12 +302,7 @@ namespace BTCPayServer.Controllers
// Find the most similar payment method to the one used for the invoice
var defaultRefund =
PaymentMethodId.GetSimilarities(
invoice.Payments.Select(o => o.GetPaymentMethodId()),
payoutMethodIds)
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
invoice.GetClosestPayoutMethodId(payoutMethodIds);
var refund = new RefundModel
{
@ -353,11 +348,7 @@ namespace BTCPayServer.Controllers
return View("_RefundModal", model);
}
var availablePaymentMethodIds = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).Where(p => _handlers.Support(p)).ToArray();
var paymentMethodId = PaymentMethodId.GetSimilarities([pmi], availablePaymentMethodIds)
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
var paymentMethodId = invoice.GetClosestPaymentMethodId([pmi]);
var paymentMethod = paymentMethodId is null ? null : invoice.GetPaymentPrompt(paymentMethodId);
if (paymentMethod?.Currency is null)
@ -367,8 +358,16 @@ namespace BTCPayServer.Controllers
}
var accounting = paymentMethod.Calculate();
decimal cryptoPaid = accounting.Paid;
decimal dueAmount = accounting.TotalDue;
var cryptoPaid = accounting.Paid;
var dueAmount = accounting.TotalDue;
// If no payment, but settled and marked, assume it has been fully paid
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatus.Settled, ExceptionStatus: InvoiceExceptionStatus.Marked })
{
cryptoPaid = accounting.TotalDue;
dueAmount = 0;
}
var paymentMethodCurrency = paymentMethod.Currency;
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;

View file

@ -1,6 +1,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection.Metadata;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices;
using NBitpayClient;
using Newtonsoft.Json;
@ -26,6 +29,28 @@ namespace BTCPayServer.Data
invoiceData.Amount = blob.Price;
invoiceData.HasTypedBlob<InvoiceEntity>().SetBlob(blob, DefaultSerializer);
}
#nullable enable
public static PayoutMethodId? GetClosestPayoutMethodId(this InvoiceData invoice, IEnumerable<PayoutMethodId> pmids)
{
var paymentMethodIds = invoice.Payments.Select(o => o.GetPaymentMethodId()).ToArray();
if (paymentMethodIds.Length == 0)
paymentMethodIds = invoice.GetBlob().GetPaymentPrompts().Select(p => p.PaymentMethodId).ToArray();
return PaymentMethodId.GetSimilarities(pmids, paymentMethodIds)
.OrderByDescending(o => o.similarity)
.Select(o => o.a)
.FirstOrDefault();
}
public static PaymentMethodId? GetClosestPaymentMethodId(this InvoiceEntity invoice, IEnumerable<PayoutMethodId> pmids)
{
var paymentMethodIds = invoice.GetPayments(false).Select(o => o.PaymentMethodId).ToArray();
if (paymentMethodIds.Length == 0)
paymentMethodIds = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToArray();
return PaymentMethodId.GetSimilarities(pmids, paymentMethodIds)
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
}
#nullable restore
public static InvoiceEntity GetBlob(this InvoiceData invoiceData)
{
#pragma warning disable CS0618 // Type or member is obsolete