mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Refund updates (#4934)
This commit is contained in:
parent
541b6cf9eb
commit
195dfc2c47
8 changed files with 342 additions and 45 deletions
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models
|
|||
{
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
OverpaidAmount,
|
||||
Fiat,
|
||||
Custom
|
||||
}
|
||||
|
@ -18,8 +19,13 @@ namespace BTCPayServer.Client.Models
|
|||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? Description { get; set; } = null;
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RefundVariant? RefundVariant { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal SubtractPercentage { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? CustomAmount { get; set; }
|
||||
public string? CustomCurrency { get; set; }
|
||||
|
|
|
@ -1950,6 +1950,82 @@ namespace BTCPayServer.Tests
|
|||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
|
||||
// test subtract percentage
|
||||
validationError = await AssertValidationError(new[] { "SubtractPercentage" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 101
|
||||
});
|
||||
});
|
||||
Assert.Contains("SubtractPercentage: Percentage must be a numeric value between 0 and 100", validationError.Message);
|
||||
|
||||
// should auto-approve
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 6.15m
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.9385m, pp.Amount);
|
||||
|
||||
// test RefundVariant.OverpaidAmount
|
||||
validationError = await AssertValidationError(new[] { "RefundVariant" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
});
|
||||
Assert.Contains("Invoice is not overpaid", validationError.Message);
|
||||
|
||||
// should auto-approve
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
method = methods.First();
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||
Money.Coins(method.Due * 2)
|
||||
);
|
||||
});
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(5);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.True(invoice.Status == InvoiceStatus.Settled);
|
||||
Assert.True(invoice.AdditionalStatus == InvoiceExceptionStatus.PaidOver);
|
||||
});
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(method.Due, pp.Amount);
|
||||
|
||||
// once more with subtract percentage
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount,
|
||||
SubtractPercentage = 21m
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.79m, pp.Amount);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -24,9 +24,16 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public string RateThenText { get; set; }
|
||||
public string FiatText { get; set; }
|
||||
public decimal FiatAmount { get; set; }
|
||||
public decimal? OverpaidAmount { get; set; }
|
||||
public string OverpaidAmountText { get; set; }
|
||||
public decimal SubtractPercentage { get; set; }
|
||||
|
||||
[Display(Name = "Specify the amount and currency for the refund")]
|
||||
public decimal CustomAmount { get; set; }
|
||||
public string CustomCurrency { get; set; }
|
||||
public string InvoiceCurrency { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public int CryptoDivisibility { get; set; }
|
||||
public int InvoiceDivisibility { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
@using BTCPayServer.Client.Models
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
|
@ -68,6 +67,91 @@
|
|||
const response = await fetch(url, { method, body })
|
||||
await handleRefundResponse(response)
|
||||
})
|
||||
|
||||
function checkCustomAmount() {
|
||||
const $refundForm = document.getElementById('RefundForm');
|
||||
const currency = $refundForm.querySelector('#CustomCurrency').value;
|
||||
const cryptoCode = $refundForm.querySelector('#CryptoCode').value;
|
||||
const invoiceCurrency = $refundForm.querySelector('#InvoiceCurrency').value;
|
||||
const amount = parseFloat($refundForm.querySelector('#CustomAmount').value);
|
||||
const fiatAmount = parseFloat($refundForm.querySelector('#FiatAmount').value);
|
||||
const cryptoAmountNow = parseFloat($refundForm.querySelector('#CryptoAmountNow').value);
|
||||
const cryptoAmountThen = parseFloat($refundForm.querySelector('#CryptoAmountThen').value);
|
||||
|
||||
let isOverpaying = false;
|
||||
if (currency === cryptoCode) {
|
||||
isOverpaying = amount > Math.max(cryptoAmountNow, cryptoAmountThen);
|
||||
} else if (currency === invoiceCurrency) {
|
||||
isOverpaying = amount > fiatAmount;
|
||||
}
|
||||
document.getElementById('CustomAmountWarning').hidden = !isOverpaying;
|
||||
}
|
||||
delegate('change', '#CustomAmount', checkCustomAmount);
|
||||
delegate('change', '#CustomCurrency', checkCustomAmount);
|
||||
|
||||
function updateSubtractPercentageResult() {
|
||||
const $refundForm = document.getElementById('RefundForm');
|
||||
const $result = document.getElementById('SubtractPercentageResult');
|
||||
const $selectedRefundOption = $refundForm.querySelector('[name="SelectedRefundOption"]:checked');
|
||||
if (!$selectedRefundOption) {
|
||||
$result.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const refundOption = $selectedRefundOption.value;
|
||||
const cryptoCode = $refundForm.querySelector('#CryptoCode').value;
|
||||
const customCurrency = $refundForm.querySelector('#CustomCurrency').value;
|
||||
const invoiceCurrency = $refundForm.querySelector('#InvoiceCurrency').value;
|
||||
const customAmount = parseFloat($refundForm.querySelector('#CustomAmount').value);
|
||||
const fiatAmount = parseFloat($refundForm.querySelector('#FiatAmount').value);
|
||||
const overpaidAmount = parseFloat($refundForm.querySelector('#OverpaidAmount').value);
|
||||
const cryptoAmountNow = parseFloat($refundForm.querySelector('#CryptoAmountNow').value);
|
||||
const cryptoAmountThen = parseFloat($refundForm.querySelector('#CryptoAmountThen').value);
|
||||
const cryptoDivisibility = parseInt($refundForm.querySelector('#CryptoDivisibility').value);
|
||||
const invoiceDivisibility = parseInt($refundForm.querySelector('#InvoiceDivisibility').value);
|
||||
const percentage = parseFloat($refundForm.querySelector('#SubtractPercentage').value);
|
||||
const isInvalid = isNaN(percentage);
|
||||
|
||||
let amount = null;
|
||||
let currency = cryptoCode;
|
||||
let divisibility = cryptoDivisibility;
|
||||
switch (refundOption) {
|
||||
case 'RateThen':
|
||||
amount = cryptoAmountThen;
|
||||
break;
|
||||
case 'CurrentRate':
|
||||
amount = cryptoAmountNow;
|
||||
break;
|
||||
case 'OverpaidAmount':
|
||||
amount = overpaidAmount;
|
||||
break;
|
||||
case 'Fiat':
|
||||
amount = fiatAmount;
|
||||
currency = invoiceCurrency;
|
||||
divisibility = invoiceDivisibility;
|
||||
break;
|
||||
case 'Custom':
|
||||
amount = customAmount;
|
||||
currency = customCurrency;
|
||||
divisibility = customCurrency === invoiceCurrency ? invoiceDivisibility : cryptoDivisibility;
|
||||
break;
|
||||
}
|
||||
|
||||
if (amount == null || isInvalid) {
|
||||
$result.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log({ refundOption, isInvalid, amount, currency })
|
||||
const reduceByAmount = (amount * (percentage / 100));
|
||||
const refundAmount = (amount - reduceByAmount).toFixed(divisibility);
|
||||
$result.innerText = `= ${refundAmount} ${currency} refund`;
|
||||
$result.hidden = false;
|
||||
}
|
||||
delegate('change', '[name="SelectedRefundOption"]', updateSubtractPercentageResult);
|
||||
delegate('change', '#SubtractPercentage', updateSubtractPercentageResult);
|
||||
delegate('change', '#CustomCurrency', updateSubtractPercentageResult);
|
||||
delegate('change', '#CustomAmount', updateSubtractPercentageResult);
|
||||
</script>
|
||||
}
|
||||
|
||||
|
@ -401,7 +485,7 @@
|
|||
<section class="mt-4 d-print-none">
|
||||
<h3 class="mb-3">Webhooks</h3>
|
||||
<div class="table-responsive-xl">
|
||||
<table class="table table-hover table-responsive-md mb-5">
|
||||
<table class="table table-hover mb-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
|
@ -478,7 +562,7 @@
|
|||
<section class="mt-4">
|
||||
<h3 class="mb-3">Refunds</h3>
|
||||
<div class="table-responsive-xl">
|
||||
<table class="table table-hover table-responsive-md mb-5">
|
||||
<table class="table table-hover mb-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pull Payment</th>
|
||||
|
|
|
@ -31,21 +31,37 @@
|
|||
<span asp-validation-for="SelectedPaymentMethod" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary w-100">Next</button>
|
||||
</div>
|
||||
<button id="ok" type="submit" class="btn btn-primary w-100">Next</button>
|
||||
}
|
||||
break;
|
||||
|
||||
case RefundSteps.SelectRate:
|
||||
<input type="hidden" asp-for="SelectedPaymentMethod"/>
|
||||
<input type="hidden" asp-for="CryptoAmountThen"/>
|
||||
<input type="hidden" asp-for="FiatAmount"/>
|
||||
<input type="hidden" asp-for="CryptoAmountNow"/>
|
||||
<input type="hidden" asp-for="FiatAmount"/>
|
||||
<input type="hidden" asp-for="OverpaidAmount"/>
|
||||
<input type="hidden" asp-for="CryptoAmountNow"/>
|
||||
<input type="hidden" asp-for="CryptoDivisibility"/>
|
||||
<input type="hidden" asp-for="CryptoCode"/>
|
||||
<input type="hidden" asp-for="InvoiceCurrency"/>
|
||||
<input type="hidden" asp-for="InvoiceDivisibility"/>
|
||||
<style>
|
||||
#CustomOption ~ .form-group { display: none; }
|
||||
#CustomOption:checked ~ .form-group { display: block; }
|
||||
.additional-options { display: none; }
|
||||
[name="SelectedRefundOption"]:checked ~ .additional-options { display: block; }
|
||||
</style>
|
||||
|
||||
<span asp-validation-for="SelectedRefundOption" class="text-danger w-100"></span>
|
||||
@if (Model.OverpaidAmount is not null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="OverpaidAmountOption" asp-for="SelectedRefundOption" type="radio" value="OverpaidAmount" class="form-check-input"/>
|
||||
<label for="OverpaidAmountOption" class="form-check-label d-flex align-items-center gap-2">@Model.OverpaidAmountText <span class="badge bg-info">Overpaid amount</span></label>
|
||||
<div class="form-text">The crypto currency amount that was overpaid.</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border" />
|
||||
}
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
|
||||
|
@ -67,31 +83,39 @@
|
|||
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CustomOption" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
|
||||
<label for="CustomOption" class="form-check-label">Custom amount</label>
|
||||
<div class="form-text">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
|
||||
<div class="form-group pt-2">
|
||||
<div class="form-group pt-2 additional-options">
|
||||
<label asp-for="CustomAmount" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="CustomAmount" type="number" step="any" asp-format="{0}" class="form-control"/>
|
||||
<input asp-for="CustomCurrency" type="text" class="form-control w-auto" currency-selection />
|
||||
</div>
|
||||
<div class="alert alert-warning my-2" hidden id="CustomAmountWarning" role="alert">
|
||||
This is an overpayment of the initial amount.
|
||||
</div>
|
||||
<span asp-validation-for="CustomAmount" class="text-danger w-100"></span>
|
||||
<span asp-validation-for="CustomCurrency" class="text-danger w-100"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span asp-validation-for="SelectedRefundOption" class="text-danger w-100"></span>
|
||||
<hr class="border" />
|
||||
<div class="form-group form-check">
|
||||
<label asp-for="SubtractPercentage" class="form-label">
|
||||
Optional: Specify the percentage by which to reduce the refund, e.g. as processing charge or to compensate for the mining fee.
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="SubtractPercentage" type="number" step=".01" min="0" class="form-control" style="flex: 0 0 10ch;" />
|
||||
<span class="input-group-text">%</span>
|
||||
<span class="input-group-text" id="SubtractPercentageResult" hidden></span>
|
||||
</div>
|
||||
<span asp-validation-for="SubtractPercentage" class="text-danger w-100"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary w-100">Create refund</button>
|
||||
</div>
|
||||
<button id="ok" type="submit" class="btn btn-primary w-100">Create refund</button>
|
||||
break;
|
||||
}
|
||||
</form>
|
||||
|
|
|
@ -702,7 +702,7 @@
|
|||
},
|
||||
"refundVariant": {
|
||||
"type": "string",
|
||||
"description": "* `RateThen`: Refund the crypto currency price, at the rate the invoice got paid.\r\n* `CurrentRate`: Refund the crypto currency price, at the current rate.\r\n*`Fiat`: Refund the invoice currency, at the rate when the refund will be sent.\r\n*`Custom`: Specify the amount, currency, and rate of the refund. (see `customAmount` and `customCurrency`)",
|
||||
"description": "* `RateThen`: Refund the crypto currency price, at the rate the invoice got paid.\r\n* `CurrentRate`: Refund the crypto currency price, at the current rate.\r\n*`Fiat`: Refund the invoice currency, at the rate when the refund will be sent.\r\n*`OverpaidAmount`: Refund the crypto currency amount that was overpaid.\r\n*`Custom`: Specify the amount, currency, and rate of the refund. (see `customAmount` and `customCurrency`)",
|
||||
"x-enumNames": [
|
||||
"RateThen",
|
||||
"CurrentRate",
|
||||
|
@ -712,10 +712,17 @@
|
|||
"enum": [
|
||||
"RateThen",
|
||||
"CurrentRate",
|
||||
"OverpaidAmount",
|
||||
"Fiat",
|
||||
"Custom"
|
||||
]
|
||||
},
|
||||
"subtractPercentage": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
"description": "Optional percentage by which to reduce the refund, e.g. as processing charge or to compensate for the mining fee.",
|
||||
"example": "2.1"
|
||||
},
|
||||
"customAmount": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
|
|
Loading…
Add table
Reference in a new issue