Update price display (#4736)

* Update price display

As proposed by @dstrukt in #4364.

* Update format

* Unify price display across the app

* Add DisplayFormatter

* Replace DisplayFormatCurrency method

* Use symbol currency format for invoice

* Unify currency formats on backend pages

* Revert recent changes

* Do not show exchange rate and fiat order amount for crypto denominations

* Fix test and add test cases
This commit is contained in:
d11n
2023-03-13 02:12:58 +01:00
committed by GitHub
parent f3d9e07c5e
commit ded0c8a3bc
33 changed files with 269 additions and 152 deletions

View File

@@ -13,6 +13,7 @@ using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models.CustodianAccountViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
@@ -32,13 +33,13 @@ namespace BTCPayServer.Controllers
{
private readonly IEnumerable<ICustodian> _custodianRegistry;
private readonly CustodianAccountRepository _custodianAccountRepository;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly BTCPayServerClient _btcPayServerClient;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly LinkGenerator _linkGenerator;
public UICustodianAccountsController(
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
UserManager<ApplicationUser> userManager,
CustodianAccountRepository custodianAccountRepository,
IEnumerable<ICustodian> custodianRegistry,
@@ -47,7 +48,7 @@ namespace BTCPayServer.Controllers
LinkGenerator linkGenerator
)
{
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_displayFormatter = displayFormatter;
_custodianAccountRepository = custodianAccountRepository;
_custodianRegistry = custodianRegistry;
_btcPayServerClient = btcPayServerClient;
@@ -144,7 +145,7 @@ namespace BTCPayServer.Controllers
if (asset.Equals(defaultCurrency))
{
assetBalance.FormattedFiatValue =
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency);
_displayFormatter.Currency(pair.Value.Qty, defaultCurrency);
assetBalance.FiatValue = pair.Value.Qty;
}
else
@@ -156,11 +157,11 @@ namespace BTCPayServer.Controllers
assetBalance.Bid = quote.Bid;
assetBalance.Ask = quote.Ask;
assetBalance.FormattedBid =
_currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset);
_displayFormatter.Currency(quote.Bid, quote.FromAsset);
assetBalance.FormattedAsk =
_currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset);
_displayFormatter.Currency(quote.Ask, quote.FromAsset);
assetBalance.FormattedFiatValue =
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid,
_displayFormatter.Currency(pair.Value.Qty * quote.Bid,
defaultCurrency);
assetBalance.FiatValue = pair.Value.Qty * quote.Bid;
}

View File

@@ -20,6 +20,7 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
@@ -137,10 +138,10 @@ namespace BTCPayServer.Controllers
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
Fiat = _displayFormatter.Currency(invoice.Price, invoice.Currency),
TaxIncluded = invoice.Metadata.TaxIncluded is null
? null
: _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
: _displayFormatter.Currency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
TypedMetadata = invoice.Metadata,
@@ -229,8 +230,8 @@ namespace BTCPayServer.Controllers
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _CurrencyNameTable.FormatCurrency(paid, i.Currency),
RateFormatted = _CurrencyNameTable.FormatCurrency(rate, i.Currency),
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,
@@ -354,8 +355,7 @@ namespace BTCPayServer.Controllers
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
model.RateThenText =
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
@@ -369,13 +369,12 @@ namespace BTCPayServer.Controllers
}
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
model.CurrentRateText =
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.FiatAmount = paidCurrency;
}
model.CustomAmount = model.FiatAmount;
model.CustomCurrency = invoice.Currency;
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency);
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
return View("_RefundModal", model);
case RefundSteps.SelectRate:
@@ -477,7 +476,6 @@ namespace BTCPayServer.Controllers
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
var overpaid = false;
var model = new InvoiceDetailsModel
{
@@ -500,15 +498,11 @@ namespace BTCPayServer.Controllers
{
PaymentMethodId = paymentMethodId,
PaymentMethod = paymentMethodId.ToPrettyString(),
Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC),
paymentMethodId.CryptoCode),
Paid = _CurrencyNameTable.DisplayFormatCurrency(
accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
paymentMethodId.CryptoCode),
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
overpaidAmount, paymentMethodId.CryptoCode),
Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data),
Rate = ExchangeRate(data.GetId().CryptoCode, data),
PaymentMethodRaw = data
};
}).ToList()
@@ -794,10 +788,10 @@ namespace BTCPayServer.Controllers
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ShowMoney(divisibility),
BtcPaid = accounting.Paid.ShowMoney(divisibility),
InvoiceCurrency = invoice.Currency,
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
IsUnsetTopUp = invoice.IsUnsetTopUp(),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
@@ -805,7 +799,7 @@ namespace BTCPayServer.Controllers
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.Metadata.ItemDesc,
Rate = ExchangeRate(paymentMethod),
Rate = ExchangeRate(network.CryptoCode, paymentMethod, DisplayFormatter.CurrencyFormat.Symbol),
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/",
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
@@ -818,7 +812,6 @@ namespace BTCPayServer.Controllers
NetworkFeeMode.Never => 0,
_ => throw new NotImplementedException()
},
BtcPaid = accounting.Paid.ShowMoney(divisibility),
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
@@ -865,23 +858,33 @@ namespace BTCPayServer.Controllers
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();
model.PaymentType = paymentMethodId.PaymentType.ToString();
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint();
return model;
}
private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity)
private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
{
var currency = invoiceEntity.Currency;
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
if (cryptoCode == invoiceEntity.Currency)
if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS"))
return null;
return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency);
return _displayFormatter.Currency(invoiceEntity.Price, currency, format);
}
private string ExchangeRate(PaymentMethod paymentMethod)
private string? ExchangeRate(string cryptoCode, PaymentMethod paymentMethod, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
{
string currency = paymentMethod.ParentEntity.Currency;
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
var currency = paymentMethod.ParentEntity.Currency;
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS"))
return null;
return _displayFormatter.Currency(paymentMethod.Rate, currency, format);
}
[HttpGet("i/{invoiceId}/status")]
@@ -1004,7 +1007,8 @@ namespace BTCPayServer.Controllers
InvoiceId = invoice.Id,
OrderId = invoice.Metadata.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
Amount = invoice.Price,
Currency = invoice.Currency,
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkSettled = state.CanMarkComplete(),
Details = InvoicePopulatePayments(invoice),

View File

@@ -45,6 +45,7 @@ namespace BTCPayServer.Controllers
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
private readonly CurrencyNameTable _CurrencyNameTable;
private readonly DisplayFormatter _displayFormatter;
readonly EventAggregator _EventAggregator;
readonly BTCPayNetworkProvider _NetworkProvider;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
@@ -61,6 +62,7 @@ namespace BTCPayServer.Controllers
public UIInvoiceController(
InvoiceRepository invoiceRepository,
WalletRepository walletRepository,
DisplayFormatter displayFormatter,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
RateFetcher rateProvider,
@@ -78,6 +80,7 @@ namespace BTCPayServer.Controllers
InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator)
{
_displayFormatter = displayFormatter;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
@@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly DisplayFormatter _displayFormatter;
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
@@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
DisplayFormatter displayFormatter,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders,
@@ -61,6 +64,7 @@ namespace BTCPayServer.Controllers
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_displayFormatter = displayFormatter;
_storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
@@ -89,7 +93,7 @@ namespace BTCPayServer.Controllers
var blob = data.GetBlob();
return new ViewPaymentRequestViewModel(data)
{
AmountFormatted = _Currencies.DisplayFormatCurrency(blob.Amount, blob.Currency)
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency)
};
}).ToList();

View File

@@ -27,6 +27,7 @@ namespace BTCPayServer.Controllers
{
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
@@ -34,6 +35,7 @@ namespace BTCPayServer.Controllers
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers,
@@ -41,6 +43,7 @@ namespace BTCPayServer.Controllers
{
_dbContextFactory = dbContextFactory;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_pullPaymentHostedService = pullPaymentHostedService;
_serializerSettings = serializerSettings;
_payoutHandlers = payoutHandlers;
@@ -79,12 +82,9 @@ namespace BTCPayServer.Controllers
{
BrandColor = storeBlob.BrandColor,
CssFileId = storeBlob.CssFileId,
AmountFormatted = _currencyNameTable.FormatCurrency(blob.Limit, blob.Currency),
AmountCollected = totalPaid,
AmountCollectedFormatted = _currencyNameTable.FormatCurrency(totalPaid, blob.Currency),
AmountDue = amountDue,
ClaimedAmount = amountDue,
AmountDueFormatted = _currencyNameTable.FormatCurrency(amountDue, blob.Currency),
CurrencyData = cd,
StartDate = pp.StartDate,
LastRefreshed = DateTime.UtcNow,
@@ -93,7 +93,6 @@ namespace BTCPayServer.Controllers
{
Id = entity.Entity.Id,
Amount = entity.Blob.Amount,
AmountFormatted = _currencyNameTable.FormatCurrency(entity.Blob.Amount, blob.Currency),
Currency = blob.Currency,
Status = entity.Entity.State,
Destination = entity.Blob.Destination,
@@ -200,8 +199,8 @@ namespace BTCPayServer.Controllers
var amount = ppBlob.Currency == "SATS" ? new Money(vm.ClaimedAmount, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) : vm.ClaimedAmount;
if (destination.destination.Amount != null && amount != destination.destination.Amount)
{
var implied = _currencyNameTable.DisplayFormatCurrency(destination.destination.Amount.Value, paymentMethodId.CryptoCode);
var provided = _currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency);
var implied = _displayFormatter.Currency(destination.destination.Amount.Value, paymentMethodId.CryptoCode, DisplayFormatter.CurrencyFormat.Symbol);
var provided = _displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol);
ModelState.AddModelError(nameof(vm.ClaimedAmount),
$"Amount implied in destination ({implied}) does not match the payout amount provided ({provided}).");
}
@@ -235,7 +234,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Severity = StatusMessageModel.StatusSeverity.Success
});

View File

@@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
@@ -49,6 +50,7 @@ namespace BTCPayServer.Controllers
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IEnumerable<IPayoutHandler> payoutHandlers,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService,
ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
@@ -56,6 +58,7 @@ namespace BTCPayServer.Controllers
_btcPayNetworkProvider = btcPayNetworkProvider;
_payoutHandlers = payoutHandlers;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_pullPaymentService = pullPaymentHostedService;
_dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings;
@@ -532,7 +535,7 @@ namespace BTCPayServer.Controllers
PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id,
Date = item.Payout.Date,
PayoutId = item.Payout.Id,
Amount = _currencyNameTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
Destination = payoutBlob.Destination
};
var handler = _payoutHandlers