Refund updates (#4934)

This commit is contained in:
d11n
2023-05-11 10:33:33 +02:00
committed by GitHub
parent 541b6cf9eb
commit 195dfc2c47
8 changed files with 342 additions and 45 deletions

View File

@@ -383,14 +383,15 @@ namespace BTCPayServer.Controllers.Greenfield
}
if (invoicePaymentMethod is null)
{
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var accounting = invoicePaymentMethod.Calculate();
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
@@ -398,8 +399,10 @@ namespace BTCPayServer.Controllers.Greenfield
store.GetStoreBlob().GetRateRules(_networkProvider),
cancellationToken
);
var cryptoCode = invoicePaymentMethod.GetId().CryptoCode;
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
var createPullPayment = new HostedServices.CreatePullPayment()
var paidAmount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
var createPullPayment = new CreatePullPayment
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
Name = request.Name ?? $"Refund {invoice.Id}",
@@ -411,37 +414,61 @@ namespace BTCPayServer.Controllers.Greenfield
if (request.RefundVariant != RefundVariant.Custom)
{
if (request.CustomAmount is not null)
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
if (request.CustomCurrency is not null)
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
}
if (request.SubtractPercentage is < 0 or > 100)
{
ModelState.AddModelError(nameof(request.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var appliedDivisibility = paymentMethodDivisibility;
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = paidAmount;
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Fiat:
appliedDivisibility = cdCurrency.Divisibility;
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = paidCurrency;
createPullPayment.AutoApproveClaims = false;
break;
case RefundVariant.OverpaidAmount:
if (invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
{
ModelState.AddModelError(nameof(request.RefundVariant), "Invoice is not overpaid");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Custom:
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0))
{
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
}
if (
@@ -472,6 +499,13 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
return this.CreateValidationError(ModelState);
}
// reduce by percentage
if (request.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (request.SubtractPercentage / 100);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
}
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);

View File

@@ -347,23 +347,39 @@ namespace BTCPayServer.Controllers
RateRules rules;
RateResult rateResult;
CreatePullPayment createPullPayment;
PaymentMethodAccounting accounting;
var pms = invoice.GetPaymentMethods();
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
var appliedDivisibility = paymentMethodDivisibility;
decimal dueAmount = default;
decimal paidAmount = default;
decimal cryptoPaid = default;
//TODO: Make this clean
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
{
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
}
if (paymentMethod != null)
{
accounting = paymentMethod.Calculate();
cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
}
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;
decimal? overpaidAmount = isPaidOver ? Math.Round(paidAmount - dueAmount, appliedDivisibility) : null;
switch (model.RefundStep)
{
case RefundSteps.SelectPaymentMethod:
model.RefundStep = RefundSteps.SelectRate;
model.Title = "How much to refund?";
var pms = invoice.GetPaymentMethods();
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
//TODO: Make this clean
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
if (paymentMethod != null && cryptoPaid != default)
{
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
}
if (paymentMethod != null)
{
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
@@ -383,8 +399,15 @@ namespace BTCPayServer.Controllers
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.FiatAmount = paidCurrency;
}
model.CryptoCode = paymentMethodId.CryptoCode;
model.CryptoDivisibility = paymentMethodDivisibility;
model.InvoiceDivisibility = cdCurrency.Divisibility;
model.InvoiceCurrency = invoice.Currency;
model.CustomAmount = model.FiatAmount;
model.CustomCurrency = invoice.Currency;
model.SubtractPercentage = 0;
model.OverpaidAmount = overpaidAmount;
model.OverpaidAmountText = overpaidAmount != null ? _displayFormatter.Currency(overpaidAmount.Value, paymentMethodId.CryptoCode) : null;
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
return View("_RefundModal", model);
@@ -399,6 +422,15 @@ namespace BTCPayServer.Controllers
var authorizedForAutoApprove = (await
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
.Succeeded;
if (model.SubtractPercentage is < 0 or > 100)
{
ModelState.AddModelError(nameof(model.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
}
if (!ModelState.IsValid)
{
return View("_RefundModal", model);
}
switch (model.SelectedRefundOption)
{
case "RateThen":
@@ -414,27 +446,47 @@ namespace BTCPayServer.Controllers
break;
case "Fiat":
appliedDivisibility = cdCurrency.Divisibility;
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = model.FiatAmount;
createPullPayment.AutoApproveClaims = false;
break;
case "OverpaidAmount":
model.Title = "How much to refund?";
model.RefundStep = RefundSteps.SelectRate;
if (isPaidOver)
{
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
}
if (overpaidAmount == null)
{
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Overpaid amount cannot be calculated");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Amount = overpaidAmount!.Value;
createPullPayment.AutoApproveClaims = true;
break;
case "Custom":
model.Title = "How much to refund?";
model.RefundStep = RefundSteps.SelectRate;
if (model.CustomAmount <= 0)
{
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
}
if (string.IsNullOrEmpty(model.CustomCurrency) ||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
{
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
}
if (!ModelState.IsValid)
{
return View("_RefundModal", model);
@@ -468,6 +520,13 @@ namespace BTCPayServer.Controllers
throw new ArgumentOutOfRangeException();
}
// reduce by percentage
if (model.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (model.SubtractPercentage / 100);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
}
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
TempData.SetStatusMessageModel(new StatusMessageModel
{