Fix fallback logic for default payment method (#2986)

This commit is contained in:
Nicolas Dorier
2021-10-18 16:56:47 +09:00
committed by GitHub
parent 262798d577
commit 3d3016fdca
8 changed files with 115 additions and 56 deletions

View File

@@ -2297,19 +2297,27 @@ namespace BTCPayServer.Tests
} }
[Fact] [Fact]
[Trait("Integration", "Integration")] [Trait("Lightning", "Lightning")]
public async Task CanSetPaymentMethodLimits() public async Task CanSetPaymentMethodLimits()
{ {
using (var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.ActivateLightning();
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess(true);
user.RegisterDerivationScheme("BTC"); 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<CheckoutExperienceViewModel>(Assert var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model); .IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
Assert.Single(vm.PaymentMethodCriteria); Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = vm.PaymentMethodCriteria.First(); var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod); Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD"; criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
@@ -2319,7 +2327,7 @@ namespace BTCPayServer.Tests
var invoice = user.BitPay.CreateInvoice( var invoice = user.BitPay.CreateInvoice(
new Invoice() new Invoice()
{ {
Price = 5.5m, Price = 4.5m,
Currency = "USD", Currency = "USD",
PosData = "posData", PosData = "posData",
OrderId = "orderId", OrderId = "orderId",
@@ -2328,7 +2336,41 @@ namespace BTCPayServer.Tests
}, Facade.Merchant); }, Facade.Merchant);
Assert.Single(invoice.CryptoInfo); 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<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().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<RedirectToActionResult>(user.GetController<StoresController>().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<InvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
Assert.Equal(lnMethod, checkout.PaymentMethodId);
// If we change store's default, it should change the checkout's default
vm.DefaultPaymentMethod = btcMethod;
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm)
.Result);
checkout = (await user.GetController<InvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
Assert.Equal(btcMethod, checkout.PaymentMethodId);
} }
} }

View File

@@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers.GreenField
Name = data.StoreName, Name = data.StoreName,
Website = data.StoreWebsite, Website = data.StoreWebsite,
SpeedPolicy = data.SpeedPolicy, SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId(_btcPayNetworkProvider)?.ToStringNormalized(), DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
//blob //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 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 //we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints

View File

@@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
//var network = invoice.Networks.GetNetwork(invoice.Currency); //var network = invoice.Networks.GetNetwork(invoice.Currency);
var cryptoCode = "BTC"; var cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode); var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider); var paymentMethodId = store.GetDefaultPaymentId();
//var network = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC"); //var network = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination(); var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination();

View File

@@ -460,18 +460,6 @@ namespace BTCPayServer.Controllers
return View(model); 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<PaymentModel?> GetInvoiceModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang) private async Task<PaymentModel?> GetInvoiceModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang)
{ {
var invoice = await _InvoiceRepository.GetInvoice(invoiceId); var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
@@ -481,19 +469,35 @@ namespace BTCPayServer.Controllers
bool isDefaultPaymentId = false; bool isDefaultPaymentId = false;
if (paymentMethodId is null) if (paymentMethodId is null)
{ {
paymentMethodId = GetDefaultInvoicePaymentId(store.GetEnabledPaymentIds(_NetworkProvider), invoice) ?? store.GetDefaultPaymentId(_NetworkProvider); var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider) ?? Array.Empty<PaymentMethodId>();
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; isDefaultPaymentId = true;
} }
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode); BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network == null && isDefaultPaymentId) if (network is null || !invoice.Support(paymentMethodId))
{
//TODO: need to look into a better way for this as it does not scale
network = _NetworkProvider.GetAll().OfType<BTCPayNetwork>().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{ {
if (!isDefaultPaymentId) if (!isDefaultPaymentId)
return null; return null;

View File

@@ -412,7 +412,10 @@ namespace BTCPayServer.Controllers
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData) 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 => .Select(o =>
new CheckoutExperienceViewModel.Format() new CheckoutExperienceViewModel.Format()
{ {
@@ -420,9 +423,7 @@ namespace BTCPayServer.Controllers
Value = o.ToString(), Value = o.ToString(),
PaymentId = o PaymentId = o
}).ToArray(); }).ToArray();
var chosen = defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase));
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
vm.PaymentMethods = 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; vm.DefaultPaymentMethod = chosen?.Value;
} }
@@ -434,7 +435,7 @@ namespace BTCPayServer.Controllers
bool needUpdate = false; bool needUpdate = false;
var blob = CurrentStore.GetStoreBlob(); var blob = CurrentStore.GetStoreBlob();
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
if (CurrentStore.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId) if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId)
{ {
needUpdate = true; needUpdate = true;
CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId);

View File

@@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -12,14 +13,10 @@ namespace BTCPayServer.Data
public static class StoreDataExtensions public static class StoreDataExtensions
{ {
#pragma warning disable CS0618 #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); PaymentMethodId.TryParse(storeData.DefaultCrypto, out var defaultPaymentId);
var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ?? return defaultPaymentId;
paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ??
paymentMethodIds.FirstOrDefault();
return chosen;
} }
public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData, BTCPayNetworkProvider networks) public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData, BTCPayNetworkProvider networks)
@@ -104,7 +101,7 @@ namespace BTCPayServer.Data
/// </summary> /// </summary>
/// <param name="paymentMethodId">The paymentMethodId</param> /// <param name="paymentMethodId">The paymentMethodId</param>
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param> /// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
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) if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId)
{ {

View File

@@ -1,4 +1,7 @@
#nullable enable
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace BTCPayServer.Payments namespace BTCPayServer.Payments
{ {
@@ -8,6 +11,13 @@ namespace BTCPayServer.Payments
/// </summary> /// </summary>
public class PaymentMethodId 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) public PaymentMethodId(string cryptoCode, PaymentType paymentType)
{ {
if (cryptoCode == null) if (cryptoCode == null)
@@ -31,23 +41,22 @@ namespace BTCPayServer.Payments
public PaymentType PaymentType { get; private set; } public PaymentType PaymentType { get; private set; }
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
PaymentMethodId item = obj as PaymentMethodId; if (obj is PaymentMethodId id)
if (item == null) return ToString().Equals(id.ToString(), StringComparison.OrdinalIgnoreCase);
return false; return false;
return ToString().Equals(item.ToString(), StringComparison.InvariantCulture);
} }
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; return true;
if (((object)a == null) || ((object)b == null)) if (a is PaymentMethodId ai && b is PaymentMethodId bi)
return ai.Equals(bi);
return false; return false;
return a.ToString() == b.ToString();
} }
public static bool operator !=(PaymentMethodId a, PaymentMethodId b) public static bool operator !=(PaymentMethodId? a, PaymentMethodId? b)
{ {
return !(a == b); return !(a == b);
} }
@@ -84,12 +93,12 @@ namespace BTCPayServer.Payments
return $"{CryptoCode} ({PaymentType.ToPrettyString()})"; return $"{CryptoCode} ({PaymentType.ToPrettyString()})";
} }
static char[] Separators = new[] { '_', '-' }; static char[] Separators = new[] { '_', '-' };
public static PaymentMethodId TryParse(string str) public static PaymentMethodId? TryParse(string? str)
{ {
TryParse(str, out var r); TryParse(str, out var r);
return r; return r;
} }
public static bool TryParse(string str, out PaymentMethodId paymentMethodId) public static bool TryParse(string? str, [MaybeNullWhen(false)] out PaymentMethodId paymentMethodId)
{ {
str ??= ""; str ??= "";
paymentMethodId = null; paymentMethodId = null;

View File

@@ -258,7 +258,13 @@ namespace BTCPayServer.Services.Invoices
public decimal Price { get; set; } public decimal Price { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public string DefaultPaymentMethod { get; set; } public string DefaultPaymentMethod { get; set; }
#nullable enable
public PaymentMethodId? GetDefaultPaymentMethod()
{
PaymentMethodId.TryParse(DefaultPaymentMethod, out var id);
return id;
}
#nullable restore
[JsonExtensionData] [JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } public IDictionary<string, JToken> AdditionalData { get; set; }