mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Add Value Criteria For Payment Method
I upgrade lightning max/bitcoin min to support better control in store. Now can have setting only enable specific payment method only if value high/low I think make code simple more too and backward compatible
This commit is contained in:
@@ -1,242 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Logs = BTCPayServer.Tests.Logging.Logs;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
[Trait("Fast", "Fast")]
|
||||
public class PaymentHandlerTest
|
||||
{
|
||||
private readonly BitcoinLikePaymentHandler handlerBTC;
|
||||
private readonly LightningLikePaymentHandler handlerLN;
|
||||
private readonly Dictionary<CurrencyPair, Task<RateResult>> currencyPairRateResult;
|
||||
|
||||
public PaymentHandlerTest(ITestOutputHelper helper)
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
|
||||
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
||||
var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
|
||||
currencyPairRateResult = new Dictionary<CurrencyPair, Task<RateResult>>();
|
||||
|
||||
var rateResultUSDBTC = new RateResult();
|
||||
rateResultUSDBTC.BidAsk = new BidAsk(1m);
|
||||
|
||||
var rateResultBTCUSD = new RateResult();
|
||||
rateResultBTCUSD.BidAsk = new BidAsk(1m);
|
||||
|
||||
currencyPairRateResult.Add(new CurrencyPair("USD", "BTC"), Task.FromResult(rateResultUSDBTC));
|
||||
currencyPairRateResult.Add(new CurrencyPair("BTC", "USD"), Task.FromResult(rateResultBTCUSD));
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
handlerBTC = new BitcoinLikePaymentHandler(null, networkProvider, null, null, null);
|
||||
handlerLN = new LightningLikePaymentHandler(null, null, networkProvider, null);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithLightningWhenInvoiceTotalUnderLightningMaxValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = null,
|
||||
LightningMaxValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" }
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(98m, MoneyUnit.BTC);
|
||||
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerLN.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CannotPayWithLightningWhenInvoiceTotalAboveLightningMaxValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = null,
|
||||
LightningMaxValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" }
|
||||
};
|
||||
var totalInvoiceAmount = new Money(102m, MoneyUnit.BTC);
|
||||
|
||||
//When
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerLN.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.NotEqual(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithLightningWhenInvoiceTotalEqualLightningMaxValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = null,
|
||||
LightningMaxValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" }
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(100m, MoneyUnit.BTC);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerLN.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithBitcoinWhenInvoiceTotalAboveOnChainMinValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" },
|
||||
LightningMaxValue = null
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(105m, MoneyUnit.BTC);
|
||||
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CannotPayWithBitcoinWhenInvoiceTotalUnderOnChainMinValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" },
|
||||
LightningMaxValue = null
|
||||
};
|
||||
var totalInvoiceAmount = new Money(98m, MoneyUnit.BTC);
|
||||
|
||||
//When
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.NotEqual(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithBitcoinWhenInvoiceTotalEqualOnChainMinValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" },
|
||||
LightningMaxValue = null
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(100m, MoneyUnit.BTC);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CannotPayWithBitcoinWhenInvoiceTotalUnderOnChainMinValueWhenLightningMaxValueIsGreater()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() { Value = 50.00m, Currency = "USD" },
|
||||
LightningMaxValue = new CurrencyValue() { Value = 100.00m, Currency = "USD" }
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(45m, MoneyUnit.BTC);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.NotEqual(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1877,7 +1877,11 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
|
||||
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
|
||||
vm.OnChainMinValue = "5 USD";
|
||||
Assert.Single(vm.PaymentMethodCriteria);
|
||||
var criteria = vm.PaymentMethodCriteria.First();
|
||||
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
|
||||
criteria.Value = "5 USD";
|
||||
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm)
|
||||
.Result);
|
||||
|
||||
@@ -1894,6 +1898,21 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||
|
||||
//test backward compat
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.PaymentMethodCriteria = new List<PaymentMethodCriteria>();
|
||||
#pragma warning disable 612
|
||||
blob.OnChainMinValue = new CurrencyValue()
|
||||
#pragma warning restore 612
|
||||
{
|
||||
Currency = "USD",
|
||||
Value = 2m
|
||||
};
|
||||
var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob);
|
||||
Assert.Single(criteriaCompat);
|
||||
Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) ));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,7 +1931,11 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
|
||||
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
|
||||
vm.LightningMaxValue = "2 USD";
|
||||
Assert.Single(vm.PaymentMethodCriteria);
|
||||
var criteria = vm.PaymentMethodCriteria.First();
|
||||
Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
|
||||
criteria.Value = "2 USD";
|
||||
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm)
|
||||
.Result);
|
||||
|
||||
@@ -1929,6 +1952,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||
|
||||
//test backward compat
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.PaymentMethodCriteria = new List<PaymentMethodCriteria>();
|
||||
#pragma warning disable 612
|
||||
blob.LightningMaxValue = new CurrencyValue()
|
||||
#pragma warning restore 612
|
||||
{
|
||||
Currency = "USD",
|
||||
Value = 2m
|
||||
};
|
||||
var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob);
|
||||
Assert.Single(criteriaCompat);
|
||||
Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance) ));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
@@ -207,11 +208,13 @@ namespace BTCPayServer.Controllers
|
||||
.Where(c => c != null))
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, entity.Currency));
|
||||
//TODO: abstract
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
|
||||
foreach (var paymentMethodCriteria in store.GetPaymentMethodCriteria(_NetworkProvider, storeBlob))
|
||||
{
|
||||
if (paymentMethodCriteria.Value != null)
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, paymentMethodCriteria.Value.Currency));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
@@ -325,17 +328,29 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
}
|
||||
|
||||
var errorMessage = await
|
||||
handler
|
||||
.IsPaymentMethodAllowedBasedOnInvoiceAmount(storeBlob, fetchingByCurrencyPair,
|
||||
paymentMethod.Calculate().Due, supportedPaymentMethod.PaymentId);
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
var criteria = store.GetPaymentMethodCriteria(_NetworkProvider, storeBlob)?.Find(methodCriteria => methodCriteria.PaymentMethod == supportedPaymentMethod.PaymentId);
|
||||
if (criteria?.Value != null)
|
||||
{
|
||||
logs.Write($"{logPrefix} {errorMessage}", InvoiceEventData.EventSeverity.Error);
|
||||
return null;
|
||||
var currentRateToCrypto =
|
||||
await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)];
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork);
|
||||
var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid;
|
||||
|
||||
if (amount < limitValueCrypto && criteria.Above)
|
||||
{
|
||||
logs.Write($"{logPrefix} invoice amount below accepted value for payment method", InvoiceEventData.EventSeverity.Error);
|
||||
return null;
|
||||
}
|
||||
if (amount > limitValueCrypto && !criteria.Above)
|
||||
{
|
||||
logs.Write($"{logPrefix} invoice amount above accepted value for payment method", InvoiceEventData.EventSeverity.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
|
||||
@@ -370,6 +370,14 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
SetCryptoCurrencies(vm, CurrentStore);
|
||||
vm.PaymentMethodCriteria =
|
||||
CurrentStore.GetPaymentMethodCriteria(_NetworkProvider, storeBlob).Select(criteria =>
|
||||
new PaymentMethodCriteriaViewModel()
|
||||
{
|
||||
PaymentMethod = criteria.PaymentMethod.ToString(),
|
||||
Type = criteria.Above? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan : PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
|
||||
Value = criteria.Value?.ToString()?? ""
|
||||
}).ToList();
|
||||
vm.CustomCSS = storeBlob.CustomCSS;
|
||||
vm.CustomLogo = storeBlob.CustomLogo;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
@@ -377,45 +385,32 @@ namespace BTCPayServer.Controllers
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
|
||||
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
|
||||
{
|
||||
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
|
||||
.Select(o => new CheckoutExperienceViewModel.Format() { Name = o.ToPrettyString(), Value = o.ToString(), PaymentId = o }).ToArray();
|
||||
.Select(o =>
|
||||
new CheckoutExperienceViewModel.Format()
|
||||
{
|
||||
Name = o.ToPrettyString(), Value = o.ToString(), PaymentId = o
|
||||
}).ToArray();
|
||||
|
||||
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
|
||||
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
|
||||
vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
|
||||
vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
|
||||
vm.DefaultPaymentMethod = chosen?.Value;
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
|
||||
{
|
||||
CurrencyValue lightningMaxValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||
{
|
||||
if (!CurrencyValue.TryParse(model.LightningMaxValue, out lightningMaxValue))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid lightning max value");
|
||||
}
|
||||
}
|
||||
|
||||
CurrencyValue onchainMinValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.OnChainMinValue))
|
||||
{
|
||||
if (!CurrencyValue.TryParse(model.OnChainMinValue, out onchainMinValue))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
|
||||
}
|
||||
}
|
||||
bool needUpdate = false;
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
|
||||
@@ -426,11 +421,33 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
SetCryptoCurrencies(model, CurrentStore);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
for (var index = 0; index < model.PaymentMethodCriteria.Count; index++)
|
||||
{
|
||||
var methodCriterion = model.PaymentMethodCriteria[index];
|
||||
if (!string.IsNullOrWhiteSpace(methodCriterion.Value))
|
||||
{
|
||||
if (!CurrencyValue.TryParse(methodCriterion.Value, out var value))
|
||||
{
|
||||
model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value, $"{methodCriterion.PaymentMethod}: invalid format (1.0 USD)", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
blob.PaymentMethodCriteria = model.PaymentMethodCriteria
|
||||
.Where(viewModel => !string.IsNullOrEmpty(viewModel.Value)).Select(viewModel =>
|
||||
{
|
||||
CurrencyValue.TryParse(viewModel.Value, out var cv);
|
||||
return new PaymentMethodCriteria() {Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod)};
|
||||
}).ToList();
|
||||
#pragma warning disable 612
|
||||
blob.LightningMaxValue = null;
|
||||
blob.OnChainMinValue = null;
|
||||
#pragma warning restore 612
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
@@ -438,8 +455,6 @@ namespace BTCPayServer.Controllers
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.ShowRecommendedFee = model.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
|
||||
@@ -90,8 +90,17 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public List<PaymentMethodCriteria> PaymentMethodCriteria
|
||||
{
|
||||
[Obsolete("Use GetPaymentMethodCriteria instead")]
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue OnChainMinValue { get; set; }
|
||||
[Obsolete]
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
@@ -202,6 +211,15 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
public class PaymentMethodCriteria
|
||||
{
|
||||
[JsonConverter(typeof(PaymentMethodIdJsonConverter))]
|
||||
public PaymentMethodId PaymentMethod { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue Value { get; set; }
|
||||
public bool Above { get; set; }
|
||||
}
|
||||
|
||||
public class RateRule_Obsolete
|
||||
{
|
||||
public RateRule_Obsolete()
|
||||
|
||||
@@ -49,6 +49,44 @@ namespace BTCPayServer.Data
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<PaymentMethodCriteria> GetPaymentMethodCriteria(this StoreData storeData, BTCPayNetworkProvider networkProvider,StoreBlob storeBlob = null)
|
||||
{
|
||||
#pragma warning disable 612
|
||||
storeBlob ??= storeData.GetStoreBlob();
|
||||
|
||||
return storeData.GetEnabledPaymentIds(networkProvider).Select(paymentMethodId=>
|
||||
{
|
||||
var matchedFromBlob =
|
||||
storeBlob.PaymentMethodCriteria?.SingleOrDefault(criteria => criteria.PaymentMethod == paymentMethodId && criteria.Value != null);
|
||||
if (matchedFromBlob is null && paymentMethodId.PaymentType == LightningPaymentType.Instance && storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
return new PaymentMethodCriteria()
|
||||
{
|
||||
Above = false,
|
||||
PaymentMethod = paymentMethodId,
|
||||
Value = storeBlob.LightningMaxValue
|
||||
};
|
||||
}
|
||||
if (matchedFromBlob is null && paymentMethodId.PaymentType == BitcoinPaymentType.Instance && storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
return new PaymentMethodCriteria()
|
||||
{
|
||||
Above = true,
|
||||
PaymentMethod = paymentMethodId,
|
||||
Value = storeBlob.OnChainMinValue
|
||||
};
|
||||
}
|
||||
|
||||
return new PaymentMethodCriteria()
|
||||
{
|
||||
PaymentMethod = paymentMethodId,
|
||||
Above = matchedFromBlob?.Above??true,
|
||||
Value = matchedFromBlob?.Value
|
||||
};
|
||||
}).ToList();
|
||||
#pragma warning restore 612
|
||||
}
|
||||
|
||||
public static bool SetStoreBlob(this StoreData storeData, StoreBlob storeBlob)
|
||||
{
|
||||
var original = new Serializer(null).ToString(storeData.GetStoreBlob());
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@@ -15,7 +17,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string Value { get; set; }
|
||||
public PaymentMethodId PaymentId { get; set; }
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList PaymentMethods { get; set; }
|
||||
|
||||
public void SetLanguages(LanguageService langService, string defaultLang)
|
||||
{
|
||||
@@ -49,15 +51,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Recommended fee confirmation target blocks")]
|
||||
[Range(1, double.PositiveInfinity)]
|
||||
public int RecommendedFeeBlockTarget { get; set; }
|
||||
|
||||
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
|
||||
[MaxLength(20)]
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
|
||||
[Display(Name = "Display lightning payment amounts in Satoshis")]
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
@@ -66,5 +60,34 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
public List<PaymentMethodCriteriaViewModel> PaymentMethodCriteria { get; set; }
|
||||
}
|
||||
|
||||
public class PaymentMethodCriteriaViewModel
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Value { get; set; }
|
||||
|
||||
public CriteriaType Type { get; set; }
|
||||
|
||||
public enum CriteriaType
|
||||
{
|
||||
GreaterThan,
|
||||
LessThan
|
||||
}
|
||||
public static string ToString(CriteriaType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case CriteriaType.GreaterThan:
|
||||
return "Greater than";
|
||||
case CriteriaType.LessThan:
|
||||
return "Less than";
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,27 +76,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
return GetPaymentMethodName(network);
|
||||
}
|
||||
|
||||
public override async Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
var currentRateToCrypto =
|
||||
await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.OnChainMinValue.Currency)];
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto =
|
||||
Money.Coins(storeBlob.OnChainMinValue.Value / currentRateToCrypto.BidAsk.Bid);
|
||||
if (amount < limitValueCrypto)
|
||||
{
|
||||
return "The amount of the invoice is too low to be paid on chain";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||
{
|
||||
return _networkProvider
|
||||
|
||||
@@ -42,10 +42,6 @@ namespace BTCPayServer.Payments
|
||||
string GetCryptoImage(PaymentMethodId paymentMethodId);
|
||||
string GetPaymentMethodName(PaymentMethodId paymentMethodId);
|
||||
|
||||
Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate,
|
||||
Money amount, PaymentMethodId paymentMethodId);
|
||||
|
||||
IEnumerable<PaymentMethodId> GetSupportedPaymentMethods();
|
||||
CheckoutUIPaymentMethodSettings GetCheckoutUISettings();
|
||||
}
|
||||
@@ -75,9 +71,6 @@ namespace BTCPayServer.Payments
|
||||
public abstract string GetCryptoImage(PaymentMethodId paymentMethodId);
|
||||
public abstract string GetPaymentMethodName(PaymentMethodId paymentMethodId);
|
||||
|
||||
public abstract Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount, PaymentMethodId paymentMethodId);
|
||||
|
||||
public abstract IEnumerable<PaymentMethodId> GetSupportedPaymentMethods();
|
||||
public virtual CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
|
||||
{
|
||||
|
||||
@@ -149,26 +149,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
.Select(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike));
|
||||
}
|
||||
|
||||
|
||||
public override async Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
var currentRateToCrypto = await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.LightningMaxValue.Currency)];
|
||||
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(storeBlob.LightningMaxValue.Value / currentRateToCrypto.BidAsk.Bid);
|
||||
if (amount > limitValueCrypto)
|
||||
{
|
||||
return "The amount of the invoice is too high to be paid with lightning";
|
||||
}
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
|
||||
StoreBlob storeBlob)
|
||||
{
|
||||
|
||||
@@ -95,13 +95,6 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
return GetPaymentMethodName(network);
|
||||
}
|
||||
|
||||
public override Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount,
|
||||
PaymentMethodId paymentMethodId)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||
{
|
||||
return _networkProvider.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
|
||||
@@ -98,13 +98,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode);
|
||||
return GetPaymentMethodName(network);
|
||||
}
|
||||
|
||||
public override Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount,
|
||||
PaymentMethodId paymentMethodId)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||
{
|
||||
return _networkProvider.GetAll()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@model CheckoutExperienceViewModel
|
||||
@using BTCPayServer.Payments
|
||||
@model CheckoutExperienceViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.Checkout, "Checkout experience");
|
||||
@@ -19,36 +20,48 @@
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<h4 class="mb-3">Payment</h4>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultPaymentMethod"></label>
|
||||
<select asp-for="DefaultPaymentMethod" asp-items="Model.CryptoCurrencies" class="form-control w-auto"></select>
|
||||
</div>
|
||||
@if (Model.PaymentMethods.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultPaymentMethod"></label>
|
||||
<select asp-for="DefaultPaymentMethod" asp-items="Model.PaymentMethods" class="form-control w-auto"></select>
|
||||
</div>
|
||||
<table class="table table-bordered table">
|
||||
<tr>
|
||||
<td colspan="3" class="text-decoration-none">
|
||||
Enable payment methods only when amount is..
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@for (var index = 0; index < Model.PaymentMethodCriteria.Count; index++)
|
||||
{
|
||||
var criteria = Model.PaymentMethodCriteria[index];
|
||||
<tr >
|
||||
<td class="border-right-0 border-left-0 pt-3">
|
||||
<input type="hidden" asp-for="PaymentMethodCriteria[index].PaymentMethod"/>
|
||||
@PaymentMethodId.Parse(criteria.PaymentMethod).ToPrettyString()
|
||||
</td>
|
||||
<td class="border-right-0 border-left-0 ">
|
||||
<select asp-for="PaymentMethodCriteria[index].Type"
|
||||
class="form-control"
|
||||
asp-items="@((PaymentMethodCriteriaViewModel.CriteriaType[]) Enum.GetValues(typeof(PaymentMethodCriteriaViewModel.CriteriaType))).Select(s => new SelectListItem(PaymentMethodCriteriaViewModel.ToString(s), s.ToString()))">
|
||||
</select>
|
||||
|
||||
</td>
|
||||
<td class="border-right-0 border-left-0">
|
||||
<input placeholder="6.15 USD" asp-for="PaymentMethodCriteria[index].Value" class="form-control my-0" style="max-width: 20ch;"/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<div class="form-check">
|
||||
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="RequiresRefundEmail" class="form-check-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="OnChainMinValue"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="OnChainMinValue" class="form-control" style="max-width: 20ch;"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">Example: 6.15 USD</span>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="OnChainMinValue" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="LightningMaxValue"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="LightningMaxValue" class="form-control" style="max-width: 20ch;"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">Example: 21.0 USD</span>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="LightningMaxValue" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input"/>
|
||||
|
||||
Reference in New Issue
Block a user