diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 70847219a..aa5784a04 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2297,19 +2297,27 @@ namespace BTCPayServer.Tests } [Fact] - [Trait("Integration", "Integration")] + [Trait("Lightning", "Lightning")] public async Task CanSetPaymentMethodLimits() { using (var tester = ServerTester.Create()) { + tester.ActivateLightning(); await tester.StartAsync(); var user = tester.NewAccount(); - user.GrantAccess(); + user.GrantAccess(true); user.RegisterDerivationScheme("BTC"); + await user.RegisterLightningNodeAsync("BTC"); + + + var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(); + var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToString(); + + // We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice var vm = Assert.IsType(Assert .IsType(user.GetController().CheckoutExperience()).Model); - Assert.Single(vm.PaymentMethodCriteria); - var criteria = vm.PaymentMethodCriteria.First(); + Assert.Equal(2, vm.PaymentMethodCriteria.Count); + var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod); criteria.Value = "5 USD"; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; @@ -2319,7 +2327,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice( new Invoice() { - Price = 5.5m, + Price = 4.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -2328,7 +2336,41 @@ namespace BTCPayServer.Tests }, Facade.Merchant); Assert.Single(invoice.CryptoInfo); - Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType); + Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); + + // Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963 + // We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default + // payment method should be LN. + vm = Assert.IsType(Assert + .IsType(user.GetController().CheckoutExperience()).Model); + vm.DefaultPaymentMethod = lnMethod; + criteria = vm.PaymentMethodCriteria.First(); + criteria.Value = "150 USD"; + criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; + criteria = vm.PaymentMethodCriteria.Skip(1).First(); + criteria.Value = "5 USD"; + criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; + Assert.IsType(user.GetController().CheckoutExperience(vm) + .Result); + invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 50m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + var checkout = (await user.GetController().Checkout(invoice.Id)).AssertViewModel(); + Assert.Equal(lnMethod, checkout.PaymentMethodId); + + // If we change store's default, it should change the checkout's default + vm.DefaultPaymentMethod = btcMethod; + Assert.IsType(user.GetController().CheckoutExperience(vm) + .Result); + checkout = (await user.GetController().Checkout(invoice.Id)).AssertViewModel(); + Assert.Equal(btcMethod, checkout.PaymentMethodId); } } diff --git a/BTCPayServer/Controllers/GreenField/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index 4078bbecd..d2155191a 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers.GreenField Name = data.StoreName, Website = data.StoreWebsite, SpeedPolicy = data.SpeedPolicy, - DefaultPaymentMethod = data.GetDefaultPaymentId(_btcPayNetworkProvider)?.ToStringNormalized(), + DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(), //blob //we do not include DefaultCurrencyPairs,Spread, PreferredExchange, RateScripting, RateScript in this model and instead opt to set it in stores/storeid/rates endpoints //we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints diff --git a/BTCPayServer/Controllers/InvoiceController.Testing.cs b/BTCPayServer/Controllers/InvoiceController.Testing.cs index 097e2bf24..26ba93fca 100644 --- a/BTCPayServer/Controllers/InvoiceController.Testing.cs +++ b/BTCPayServer/Controllers/InvoiceController.Testing.cs @@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers //var network = invoice.Networks.GetNetwork(invoice.Currency); var cryptoCode = "BTC"; var network = _NetworkProvider.GetNetwork(cryptoCode); - var paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider); + var paymentMethodId = store.GetDefaultPaymentId(); //var network = NetworkProvider.GetNetwork("BTC"); var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination(); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index ce9552898..ff0060137 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -460,18 +460,6 @@ namespace BTCPayServer.Controllers return View(model); } - private PaymentMethodId GetDefaultInvoicePaymentId( - PaymentMethodId[] paymentMethodIds, - InvoiceEntity invoice - ) - { - PaymentMethodId.TryParse(invoice.DefaultPaymentMethod, out var defaultPaymentId); - - return paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ?? - paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ?? - paymentMethodIds.FirstOrDefault(); - } - private async Task GetInvoiceModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang) { var invoice = await _InvoiceRepository.GetInvoice(invoiceId); @@ -481,19 +469,35 @@ namespace BTCPayServer.Controllers bool isDefaultPaymentId = false; if (paymentMethodId is null) { - paymentMethodId = GetDefaultInvoicePaymentId(store.GetEnabledPaymentIds(_NetworkProvider), invoice) ?? store.GetDefaultPaymentId(_NetworkProvider); + var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider) ?? Array.Empty(); + PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod(); + PaymentMethodId? storePaymentId = store.GetDefaultPaymentId(); + if (invoicePaymentId is PaymentMethodId) + { + if (enabledPaymentIds.Contains(invoicePaymentId)) + paymentMethodId = invoicePaymentId; + } + if (paymentMethodId is null && storePaymentId is PaymentMethodId) + { + if (enabledPaymentIds.Contains(storePaymentId)) + paymentMethodId = storePaymentId; + } + if (paymentMethodId is null && invoicePaymentId is PaymentMethodId) + { + paymentMethodId = invoicePaymentId.FindNearest(enabledPaymentIds); + } + if (paymentMethodId is null && storePaymentId is PaymentMethodId) + { + paymentMethodId = storePaymentId.FindNearest(enabledPaymentIds); + } + if (paymentMethodId is null) + { + paymentMethodId = enabledPaymentIds.First(); + } isDefaultPaymentId = true; } BTCPayNetworkBase network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); - if (network == null && isDefaultPaymentId) - { - //TODO: need to look into a better way for this as it does not scale - network = _NetworkProvider.GetAll().OfType().FirstOrDefault(); - paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); - } - if (invoice == null || network == null) - return null; - if (!invoice.Support(paymentMethodId)) + if (network is null || !invoice.Support(paymentMethodId)) { if (!isDefaultPaymentId) return null; diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 3dcc7fd82..ef0fea7a9 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -412,7 +412,10 @@ namespace BTCPayServer.Controllers void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData) { - var choices = storeData.GetEnabledPaymentIds(_NetworkProvider) + var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider); + var defaultPaymentId = storeData.GetDefaultPaymentId(); + var defaultChoice = defaultPaymentId is PaymentMethodId ? defaultPaymentId.FindNearest(enabled) : null; + var choices = enabled .Select(o => new CheckoutExperienceViewModel.Format() { @@ -420,9 +423,7 @@ namespace BTCPayServer.Controllers Value = o.ToString(), PaymentId = o }).ToArray(); - - var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider); - var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId); + var chosen = defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase)); vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value); vm.DefaultPaymentMethod = chosen?.Value; } @@ -434,7 +435,7 @@ namespace BTCPayServer.Controllers bool needUpdate = false; var blob = CurrentStore.GetStoreBlob(); var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); - if (CurrentStore.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId) + if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId) { needUpdate = true; CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index 38cf0ad66..b387ed7ec 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -12,14 +13,10 @@ namespace BTCPayServer.Data public static class StoreDataExtensions { #pragma warning disable CS0618 - public static PaymentMethodId GetDefaultPaymentId(this StoreData storeData, BTCPayNetworkProvider networks) + public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData) { - PaymentMethodId[] paymentMethodIds = storeData.GetEnabledPaymentIds(networks); PaymentMethodId.TryParse(storeData.DefaultCrypto, out var defaultPaymentId); - var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ?? - paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ?? - paymentMethodIds.FirstOrDefault(); - return chosen; + return defaultPaymentId; } public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData, BTCPayNetworkProvider networks) @@ -104,7 +101,7 @@ namespace BTCPayServer.Data /// /// The paymentMethodId /// The payment method, or null to remove - public static void SetSupportedPaymentMethod(this StoreData storeData, PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod) + public static void SetSupportedPaymentMethod(this StoreData storeData, PaymentMethodId? paymentMethodId, ISupportedPaymentMethod? supportedPaymentMethod) { if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId) { diff --git a/BTCPayServer/Payments/PaymentMethodId.cs b/BTCPayServer/Payments/PaymentMethodId.cs index 8724ceb48..8d9f7eb76 100644 --- a/BTCPayServer/Payments/PaymentMethodId.cs +++ b/BTCPayServer/Payments/PaymentMethodId.cs @@ -1,4 +1,7 @@ +#nullable enable using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace BTCPayServer.Payments { @@ -8,6 +11,13 @@ namespace BTCPayServer.Payments /// public class PaymentMethodId { + public PaymentMethodId? FindNearest(PaymentMethodId[] others) + { + if (others is null) + throw new ArgumentNullException(nameof(others)); + return others.FirstOrDefault(f => f == this) ?? + others.FirstOrDefault(f => f.CryptoCode == CryptoCode); + } public PaymentMethodId(string cryptoCode, PaymentType paymentType) { if (cryptoCode == null) @@ -31,23 +41,22 @@ namespace BTCPayServer.Payments public PaymentType PaymentType { get; private set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - PaymentMethodId item = obj as PaymentMethodId; - if (item == null) - return false; - return ToString().Equals(item.ToString(), StringComparison.InvariantCulture); + if (obj is PaymentMethodId id) + return ToString().Equals(id.ToString(), StringComparison.OrdinalIgnoreCase); + return false; } - public static bool operator ==(PaymentMethodId a, PaymentMethodId b) + public static bool operator ==(PaymentMethodId? a, PaymentMethodId? b) { - if (System.Object.ReferenceEquals(a, b)) + if (a is null && b is null) return true; - if (((object)a == null) || ((object)b == null)) - return false; - return a.ToString() == b.ToString(); + if (a is PaymentMethodId ai && b is PaymentMethodId bi) + return ai.Equals(bi); + return false; } - public static bool operator !=(PaymentMethodId a, PaymentMethodId b) + public static bool operator !=(PaymentMethodId? a, PaymentMethodId? b) { return !(a == b); } @@ -84,12 +93,12 @@ namespace BTCPayServer.Payments return $"{CryptoCode} ({PaymentType.ToPrettyString()})"; } static char[] Separators = new[] { '_', '-' }; - public static PaymentMethodId TryParse(string str) + public static PaymentMethodId? TryParse(string? str) { TryParse(str, out var r); return r; } - public static bool TryParse(string str, out PaymentMethodId paymentMethodId) + public static bool TryParse(string? str, [MaybeNullWhen(false)] out PaymentMethodId paymentMethodId) { str ??= ""; paymentMethodId = null; diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index a0bf19012..063547b41 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -258,7 +258,13 @@ namespace BTCPayServer.Services.Invoices public decimal Price { get; set; } public string Currency { get; set; } public string DefaultPaymentMethod { get; set; } - +#nullable enable + public PaymentMethodId? GetDefaultPaymentMethod() + { + PaymentMethodId.TryParse(DefaultPaymentMethod, out var id); + return id; + } +#nullable restore [JsonExtensionData] public IDictionary AdditionalData { get; set; }