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]
[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<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().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<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,
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

View File

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

View File

@@ -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<PaymentModel?> 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>();
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<BTCPayNetworkBase>(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<BTCPayNetwork>().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;

View File

@@ -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);

View File

@@ -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
/// </summary>
/// <param name="paymentMethodId">The paymentMethodId</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)
{

View File

@@ -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
/// </summary>
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)
if (obj is PaymentMethodId id)
return ToString().Equals(id.ToString(), StringComparison.OrdinalIgnoreCase);
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;
if (((object)a == null) || ((object)b == null))
if (a is PaymentMethodId ai && b is PaymentMethodId bi)
return ai.Equals(bi);
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);
}
@@ -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;

View File

@@ -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<string, JToken> AdditionalData { get; set; }