mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Fix fallback logic for default payment method (#2986)
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 false;
|
return ai.Equals(bi);
|
||||||
return a.ToString() == b.ToString();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user