From 229a4ea56c91a35f4b77effc40adebadb0f44d13 Mon Sep 17 00:00:00 2001 From: d11n Date: Tue, 10 Oct 2023 05:28:00 +0200 Subject: [PATCH] Invoice: Improve payment details (#5362) * Invoice: Improve payment details Clearer description and display, especially for overpayments. Closes #5207. * Further refinements * Test fix --- BTCPayServer.Tests/ThirdPartyTests.cs | 18 +++-- .../Controllers/UIInvoiceController.UI.cs | 35 ++++++---- .../InvoicingModels/InvoiceDetailsModel.cs | 5 +- .../Bitcoin/ViewBitcoinLikePaymentData.cshtml | 6 +- .../ViewLightningLikePaymentData.cshtml | 6 +- .../Monero/ViewMoneroLikePaymentData.cshtml | 62 +++++++++--------- .../Zcash/ViewZcashLikePaymentData.cshtml | 63 +++++++++--------- BTCPayServer/Views/UIInvoice/Invoice.cshtml | 29 +++++---- .../Views/UIInvoice/ListInvoices.cshtml | 4 +- .../ListInvoicesPaymentsPartial.cshtml | 65 ++++++++++++++++--- .../Views/UIStorePullPayments/Payouts.cshtml | 2 +- .../UIStorePullPayments/PullPayments.cshtml | 2 +- .../Views/UIWallets/WalletTransactions.cshtml | 2 +- BTCPayServer/wwwroot/main/site.css | 2 +- 14 files changed, 185 insertions(+), 116 deletions(-) diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index 67df2e086..9264e7808 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -298,7 +298,7 @@ retry: var fetcher = new RateFetcher(factory); var provider = new BTCPayNetworkProvider(ChainName.Mainnet); var b = new StoreBlob(); - string[] temporarilyBroken = { "UGX" }; + string[] temporarilyBroken = { "COP", "UGX" }; foreach (var k in StoreBlob.RecommendedExchanges) { b.DefaultCurrency = k.Key; @@ -307,14 +307,20 @@ retry: var result = fetcher.FetchRates(pairs, rules, default); foreach ((CurrencyPair key, Task value) in result) { + TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}"); + var rateResult = await value; + var hasRate = rateResult.BidAsk != null; + if (temporarilyBroken.Contains(k.Key)) { - TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken"); - continue; + if (!hasRate) + { + TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken"); + continue; + } + TestLogs.LogInformation($"Note: {key} is marked as temporarily broken, but the rate is available"); } - var rateResult = await value; - TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}"); - Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}"); + Assert.True(hasRate, $"Impossible to get the rate {rateResult.EvaluatedRule}"); } } } diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index 81235dad8..cba101fee 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -165,6 +165,8 @@ namespace BTCPayServer.Controllers model.CryptoPayments = details.CryptoPayments; model.Payments = details.Payments; model.Overpaid = details.Overpaid; + model.StillDue = details.StillDue; + model.HasRates = details.HasRates; if (additionalData.ContainsKey("receiptData")) { @@ -565,37 +567,42 @@ namespace BTCPayServer.Controllers private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice) { var overpaid = false; + var stillDue = false; + var hasRates = false; var model = new InvoiceDetailsModel { Archived = invoice.Archived, Payments = invoice.GetPayments(false), - Overpaid = true, CryptoPayments = invoice.GetPaymentMethods().Select( data => { var accounting = data.Calculate(); var paymentMethodId = data.GetId(); + var hasPayment = accounting.CryptoPaid > 0; var overpaidAmount = accounting.OverpaidHelper; - - if (overpaidAmount > 0) - { - overpaid = true; - } + var rate = ExchangeRate(data.GetId().CryptoCode, data); + + if (rate is not null) hasRates = true; + if (hasPayment && overpaidAmount > 0) overpaid = true; + if (hasPayment && accounting.Due > 0) stillDue = true; return new InvoiceDetailsModel.CryptoPayment { + Rate = rate, + PaymentMethodRaw = data, PaymentMethodId = paymentMethodId, PaymentMethod = paymentMethodId.ToPrettyString(), - Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode), - Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode), - Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode), - Address = data.GetPaymentMethodDetails().GetPaymentDestination(), - Rate = ExchangeRate(data.GetId().CryptoCode, data), - PaymentMethodRaw = data + TotalDue = _displayFormatter.Currency(accounting.TotalDue, paymentMethodId.CryptoCode), + Due = hasPayment ? _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode) : null, + Paid = hasPayment ? _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode) : null, + Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode) : null, + Address = data.GetPaymentMethodDetails().GetPaymentDestination() }; - }).ToList() + }).ToList(), + Overpaid = overpaid, + StillDue = stillDue, + HasRates = hasRates }; - model.Overpaid = overpaid; return model; } diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 9a02d8d6e..d6e260366 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -41,6 +41,7 @@ namespace BTCPayServer.Models.InvoicingModels public class CryptoPayment { public string PaymentMethod { get; set; } + public string TotalDue { get; set; } public string Due { get; set; } public string Paid { get; set; } public string Address { get; internal set; } @@ -138,6 +139,8 @@ namespace BTCPayServer.Models.InvoicingModels public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid; public List Refunds { get; set; } public bool ShowReceipt { get; set; } - public bool Overpaid { get; set; } = false; + public bool Overpaid { get; set; } + public bool StillDue { get; set; } + public bool HasRates { get; set; } } } diff --git a/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml index f5a1ed063..6e5ddcd8f 100644 --- a/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml @@ -59,7 +59,7 @@
On-Chain Payments
- + @@ -75,7 +75,7 @@ } - + @@ -96,7 +96,7 @@ } + @foreach (var payment in payments) + { + + + + + + + + } + +
Crypto IndexConfirmationsAmountPaid
@payment.Confirmations - @DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto) + @DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto) @if (!string.IsNullOrEmpty(payment.AdditionalInformation)) {
(@payment.AdditionalInformation)
diff --git a/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml index 599fea6f3..eb947b06c 100644 --- a/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml @@ -30,13 +30,13 @@
Off-Chain Payments
- + - + @@ -52,7 +52,7 @@ } diff --git a/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml index 19992951c..e89bb8c47 100644 --- a/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml @@ -1,10 +1,12 @@ @using System.Globalization +@using BTCPayServer.Services @using BTCPayServer.Services.Altcoins.Monero.Payments @using BTCPayServer.Services.Altcoins.Monero.UI +@inject DisplayFormatter DisplayFormatter @model IEnumerable @{ - var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == MoneroPaymentType.Instance).Select(payment => + var payments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == MoneroPaymentType.Instance).Select(payment => { var m = new MoneroPaymentViewModel(); var onChainPaymentData = payment.GetCryptoPaymentData() as MoneroLikePaymentData; @@ -26,37 +28,37 @@ m.ReceivedTime = payment.ReceivedTime; m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId); return m; - }); + }).ToList(); } -@if (onchainPayments.Any()) +@if (payments.Any()) { -
Monero Payments
-
Crypto Type Destination Payment ProofAmountPaid
- @payment.Amount + @payment.Amount
- - - - - - - - - - - @foreach (var payment in onchainPayments) - { - - - - - - +
+
Monero Payments
+
CryptoDeposit addressAmountTransaction IdConfirmations
@payment.Crypto@payment.DepositAddress@payment.Amount - - @payment.TransactionId - - @payment.Confirmations
+ + + + + + + - } - -
CryptoDestinationPayment ProofConfirmationsPaid
+ +
@payment.Crypto@payment.Confirmations + @DisplayFormatter.Currency(payment.Amount, payment.Crypto) +
+ } diff --git a/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml index 2d0d2f5ab..24f6ec9f1 100644 --- a/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml @@ -1,10 +1,13 @@ @using System.Globalization +@using BTCPayServer.Components.TruncateCenter +@using BTCPayServer.Services @using BTCPayServer.Services.Altcoins.Zcash.Payments @using BTCPayServer.Services.Altcoins.Zcash.UI +@inject DisplayFormatter DisplayFormatter @model IEnumerable @{ - var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == ZcashPaymentType.Instance).Select(payment => + var payments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == ZcashPaymentType.Instance).Select(payment => { var m = new ZcashPaymentViewModel(); var onChainPaymentData = payment.GetCryptoPaymentData() as ZcashLikePaymentData; @@ -26,37 +29,37 @@ m.ReceivedTime = payment.ReceivedTime; m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId); return m; - }); + }).ToList(); } -@if (onchainPayments.Any()) +@if (payments.Any()) { -
Zcash Payments
- - - - - - - - - - - - @foreach (var payment in onchainPayments) - { - - - - - - +
+
Zcash Payments
+
CryptoDeposit addressAmountTransaction IdConfirmations
@payment.Crypto@payment.DepositAddress@payment.Amount - - @payment.TransactionId - - @payment.Confirmations
+ + + + + + + - } - -
CryptoDestinationPayment ProofConfirmationsPaid
+ + + @foreach (var payment in payments) + { + + @payment.Crypto + + + @payment.Confirmations + + @DisplayFormatter.Currency(payment.Amount, payment.Crypto) + + + } + + + } diff --git a/BTCPayServer/Views/UIInvoice/Invoice.cshtml b/BTCPayServer/Views/UIInvoice/Invoice.cshtml index cce2b8568..a502e9619 100644 --- a/BTCPayServer/Views/UIInvoice/Invoice.cshtml +++ b/BTCPayServer/Views/UIInvoice/Invoice.cshtml @@ -327,7 +327,7 @@ @Model.TransactionSpeed - Total Fiat Due + Total Amount Due @Model.Fiat @if (!string.IsNullOrEmpty(Model.RefundEmail)) @@ -506,7 +506,8 @@ }
- +
+

Invoice Summary

@@ -638,20 +639,22 @@

Events

- - - - + + + + - @foreach (var evt in Model.Events) - { - - - - - } + @foreach (var evt in Model.Events) + { + + + + + }
DateMessage
DateMessage
@evt.Timestamp.ToBrowserDate()@evt.Message
@evt.Timestamp.ToBrowserDate()@evt.Message
+
+
diff --git a/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml b/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml index 86a9d9c6d..3ab842bd2 100644 --- a/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml @@ -32,8 +32,8 @@ @section PageHeadContent {