Add ability to set default currency for a store (#2998)

This commit is contained in:
Nicolas Dorier 2021-10-20 23:17:40 +09:00 committed by GitHub
parent 407f26b1dc
commit 4cf3249e0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 129 additions and 64 deletions

View File

@ -29,8 +29,7 @@ namespace BTCPayServer.Client.Models
public string LightningDescriptionTemplate { get; set; }
public double PaymentTolerance { get; set; } = 0;
public bool AnyoneCanCreateInvoice { get; set; }
public string DefaultCurrency { get; set; }
public bool RequiresRefundEmail { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }

View File

@ -198,6 +198,7 @@ namespace BTCPayServer.Tests
coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_EUR"), new BidAsk(4000m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m)));

View File

@ -1073,7 +1073,7 @@ namespace BTCPayServer.Tests
//create
//validation errors
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
{
await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } });
});

View File

@ -2296,6 +2296,47 @@ namespace BTCPayServer.Tests
Assert.False(CurrencyValue.TryParse("1.501", out result));
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseDefaultCurrency()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s =>
{
Assert.Equal("USD", s.DefaultCurrency);
s.DefaultCurrency = "EUR";
});
var client = await user.CreateClient();
// with greenfield
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest());
Assert.Equal("EUR", invoice.Currency);
Assert.Equal(InvoiceType.TopUp, invoice.Type);
// with bitpay api
var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice());
Assert.Equal("EUR", invoice2.Currency);
// via UI
var controller = user.GetController<InvoiceController>();
var model = await controller.CreateInvoice();
(await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType<RedirectToActionResult>();
invoice = await client.GetInvoice(user.StoreId, controller.CreatedInvoiceId);
Assert.Equal("EUR", invoice.Currency);
Assert.Equal(InvoiceType.TopUp, invoice.Type);
// Check that the SendWallet use the default currency
var walletController = user.GetController<WalletsController>();
var walletSend = await walletController.WalletSend(new WalletId(user.StoreId, "BTC")).AssertViewModelAsync<WalletSendModel>();
Assert.Equal("EUR", walletSend.Fiat);
}
}
[Fact]
[Trait("Lightning", "Lightning")]
public async Task CanSetPaymentMethodLimits()
@ -3226,16 +3267,22 @@ namespace BTCPayServer.Tests
e => e.CurrencyPair == new CurrencyPair("BTC", "AGM") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 AGM
}
else if (result.ExpectedName == "ripio")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "ARS") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 ARS
}
else
{
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDC") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDC") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// We are not showing a directly implemented exchange as directly implemented in the UI

View File

@ -163,11 +163,6 @@ namespace BTCPayServer.Controllers.GreenField
{
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
}
if (string.IsNullOrEmpty(request.Currency))
{
ModelState.AddModelError(nameof(request.Currency), "Currency is required");
}
request.Checkout = request.Checkout ?? new CreateInvoiceRequest.CheckoutOptions();
if (request.Checkout.PaymentMethods?.Any() is true)
{

View File

@ -105,6 +105,7 @@ namespace BTCPayServer.Controllers.GreenField
}
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymnetMethodId);
ToModel(request, store, defaultPaymnetMethodId);
await _storeRepository.UpdateStore(store);
return Ok(FromModel(store));
@ -150,7 +151,6 @@ namespace BTCPayServer.Controllers.GreenField
private static void ToModel(StoreBaseData restModel, Data.StoreData model, PaymentMethodId defaultPaymentMethod)
{
var blob = model.GetStoreBlob();
model.StoreName = restModel.Name;
model.StoreName = restModel.Name;
model.StoreWebsite = restModel.Website;
@ -163,6 +163,7 @@ namespace BTCPayServer.Controllers.GreenField
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
blob.NetworkFeeMode = restModel.NetworkFeeMode;
blob.DefaultCurrency = restModel.DefaultCurrency;
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;

View File

@ -685,6 +685,8 @@ namespace BTCPayServer.Controllers
}
readonly ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
public string CreatedInvoiceId;
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
{
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
@ -874,6 +876,7 @@ namespace BTCPayServer.Controllers
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
CreatedInvoiceId = result.Data.Id;
return RedirectToAction(nameof(ListInvoices));
}
catch (BitpayHttpException ex)

View File

@ -94,7 +94,6 @@ namespace BTCPayServer.Controllers
{
throw new BitpayHttpException(400, "The expirationTime is set too soon");
}
invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD";
entity.Metadata.OrderId = invoice.OrderId;
entity.Metadata.PosData = invoice.PosData;
entity.ServerUrl = serverUrl;
@ -164,7 +163,6 @@ namespace BTCPayServer.Controllers
if (invoice.Metadata != null)
entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata);
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD";
entity.Currency = invoice.Currency;
if (invoice.Amount is decimal v)
{
@ -199,7 +197,10 @@ namespace BTCPayServer.Controllers
{
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info);
var storeBlob = store.GetStoreBlob();
if (string.IsNullOrEmpty(entity.Currency))
entity.Currency = storeBlob.DefaultCurrency;
entity.Currency = entity.Currency.Trim().ToUpperInvariant();
entity.Price = Math.Max(0.0m, entity.Price);
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(entity.Currency, false);
if (currencyInfo != null)
@ -218,7 +219,6 @@ namespace BTCPayServer.Controllers
}
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
var storeBlob = store.GetStoreBlob();
if (entity.Metadata.BuyerEmail != null)
{

View File

@ -555,6 +555,7 @@ namespace BTCPayServer.Controllers
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.DefaultCurrency = storeBlob.DefaultCurrency;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
@ -605,6 +606,7 @@ namespace BTCPayServer.Controllers
}
var blob = CurrentStore.GetStoreBlob();
blob.DefaultCurrency = model.DefaultCurrency;
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration);

View File

@ -444,7 +444,7 @@ namespace BTCPayServer.Controllers
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, storeData.DefaultCurrency);
double.TryParse(defaultAmount, out var amount);
var model = new WalletSendModel()
{
@ -1040,19 +1040,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction();
}
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
return null;
try
{
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch (ArgumentException) { }
return null;
}
public StoreData CurrentStore
{
get

View File

@ -39,6 +39,20 @@ namespace BTCPayServer.Data
public bool RedirectAutomatically { get; set; }
public bool ShowRecommendedFee { get; set; }
public int RecommendedFeeBlockTarget { get; set; }
string _DefaultCurrency;
public string DefaultCurrency
{
get
{
return string.IsNullOrEmpty(_DefaultCurrency) ? "USD" : _DefaultCurrency;
}
set
{
_DefaultCurrency = value;
if (!string.IsNullOrEmpty(_DefaultCurrency))
_DefaultCurrency = _DefaultCurrency.Trim().ToUpperInvariant();
}
}
CurrencyPair[] _DefaultCurrencyPairs;
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]

View File

@ -9,17 +9,10 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class CreateInvoiceModel
{
public CreateInvoiceModel()
{
Currency = "USD";
}
public decimal? Amount
{
get; set;
}
[Required]
public string Currency
{
get; set;

View File

@ -50,6 +50,10 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Default currency")]
[MaxLength(10)]
public string DefaultCurrency { get; set; }
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }

View File

@ -40,8 +40,8 @@
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="form-label" data-required></label>
<input asp-for="Currency" class="form-control" required />
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">

View File

@ -182,12 +182,17 @@
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" class="form-control" />
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
</div>
@if (Model.IsOnchainSetup || Model.IsLightningSetup)
{
<h4 class="mt-5 mb-3">Payment</h4>
<div class="form-group d-flex align-items-center">
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="btcpay-toggle me-2"/>
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="btcpay-toggle me-2" />
<label asp-for="AnyoneCanCreateInvoice" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#allow-anyone-to-create-invoice" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
@ -210,7 +215,7 @@
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;"/>
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;" />
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
@ -221,7 +226,7 @@
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;"/>
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;" />
<span class="input-group-text">percent</span>
</div>
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
@ -233,7 +238,7 @@
{
<div class="form-group">
<div class="d-flex align-items-center">
<input asp-for="PayJoinEnabled" type="checkbox" class="btcpay-toggle me-2"/>
<input asp-for="PayJoinEnabled" type="checkbox" class="btcpay-toggle me-2" />
<label asp-for="PayJoinEnabled" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/Payjoin/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
@ -248,7 +253,7 @@
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;"/>
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;" />
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
@ -270,7 +275,7 @@
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
</div>
<div class="form-check my-1">
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input"/>
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input" />
<label asp-for="ShowRecommendedFee" class="form-check-label"></label>
<p class="form-text text-muted mb-0">Fee will be shown for BTC and LTC onchain payments only.</p>
</div>
@ -285,20 +290,20 @@
{
<h5 class="mt-5 mb-3">Lightning</h5>
<div class="form-check my-1">
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input"/>
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input" />
<label asp-for="LightningAmountInSatoshi" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input"/>
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input" />
<label asp-for="LightningPrivateRouteHints" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input"/>
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input" />
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</div>
<div class="form-group mt-3">
<label asp-for="LightningDescriptionTemplate" class="form-label"></label>
<input asp-for="LightningDescriptionTemplate" class="form-control"/>
<input asp-for="LightningDescriptionTemplate" class="form-control" />
<span asp-validation-for="LightningDescriptionTemplate" class="text-danger"></span>
<p class="form-text text-muted">
Available placeholders:

View File

@ -748,11 +748,6 @@
},
"InvoiceDataBase": {
"properties": {
"currency": {
"type": "string",
"nullable": true,
"description": "The currency the invoice will use"
},
"metadata": {
"$ref": "#/components/schemas/InvoiceMetadata"
},
@ -787,7 +782,13 @@
"amount": {
"type": "string",
"format": "decimal",
"description": "The amount of the invoice"
"description": "The amount of the invoice",
"example": "5.00"
},
"currency": {
"type": "string",
"description": "The currency of the invoice",
"example": "USD"
},
"type": {
"$ref": "#/components/schemas/InvoiceType",
@ -1000,7 +1001,14 @@
"type": "string",
"format": "decimal",
"nullable": true,
"description": "The amount of the invoice. If null or unspecified, the invoice will be a top-up invoice. (ie. The invoice will consider any payment as a full payment)"
"description": "The amount of the invoice. If null or unspecified, the invoice will be a top-up invoice. (ie. The invoice will consider any payment as a full payment)",
"example": "5.00"
},
"currency": {
"type": "string",
"description": "The currency of the invoice (if null, empty or unspecified, the currency will be the store's settings default)'",
"nullable": true,
"example": "USD"
},
"additionalSearchTerms": {
"type": "array",

View File

@ -283,19 +283,25 @@
"description": "The absolute url of the store",
"format": "url"
},
"defaultCurrency": {
"type": "string",
"description": "The default currency of the store",
"default": "USD",
"example": "USD"
},
"invoiceExpiration": {
"default": 900,
"minimum": 60,
"maximum": 2073600,
"description": "The time after which an invoice is considered expired if not paid. The value will be rounded down to a minute.",
"allOf": [ {"$ref": "#/components/schemas/TimeSpanSeconds"}]
"allOf": [ { "$ref": "#/components/schemas/TimeSpanSeconds" } ]
},
"monitoringExpiration": {
"default": 3600,
"minimum": 600,
"maximum": 2073600,
"description": "The time after which an invoice which has been paid but not confirmed will be considered invalid. The value will be rounded down to a minute.",
"allOf": [ {"$ref": "#/components/schemas/TimeSpanSeconds"}]
"allOf": [ { "$ref": "#/components/schemas/TimeSpanSeconds" } ]
},
"speedPolicy": {
"$ref": "#/components/schemas/SpeedPolicy"