mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Invoice: Improve payment details (#5362)
* Invoice: Improve payment details Clearer description and display, especially for overpayments. Closes #5207. * Further refinements * Test fix
This commit is contained in:
parent
f20e6d3768
commit
229a4ea56c
14 changed files with 185 additions and 116 deletions
|
@ -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<RateResult> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<RefundData> 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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<h5>On-Chain Payments</h5>
|
||||
<div class="invoice-payments table-responsive mt-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-100px">Index</th>
|
||||
|
@ -75,7 +75,7 @@
|
|||
</th>
|
||||
}
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Amount</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -96,7 +96,7 @@
|
|||
}
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto)</span>
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto)</span>
|
||||
@if (!string.IsNullOrEmpty(payment.AdditionalInformation))
|
||||
{
|
||||
<div>(@payment.AdditionalInformation)</div>
|
||||
|
|
|
@ -30,13 +30,13 @@
|
|||
<h5>Off-Chain Payments</h5>
|
||||
<div class="invoice-payments table-responsive mt-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-100px">Type</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="w-150px text-end">Amount</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -52,7 +52,7 @@
|
|||
<vc:truncate-center text="@payment.PaymentProof" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive>@payment.Amount</span>
|
||||
<span data-sensitive class="text-success">@payment.Amount</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
|
|
@ -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<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||
|
||||
@{
|
||||
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())
|
||||
{
|
||||
<h5>Monero Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in onchainPayments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td>@payment.Amount</td>
|
||||
<td>
|
||||
<a href="@payment.TransactionLink" class="text-break" target="_blank" rel="noreferrer noopener">
|
||||
@payment.TransactionId
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
<section>
|
||||
<h5>Monero Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in payments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
|
||||
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Crypto)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
|
|
|
@ -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<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||
|
||||
@{
|
||||
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())
|
||||
{
|
||||
<h5>Zcash Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in onchainPayments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td>@payment.Amount</td>
|
||||
<td>
|
||||
<a href="@payment.TransactionLink" class="text-break" target="_blank" rel="noreferrer noopener">
|
||||
@payment.TransactionId
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
<section>
|
||||
<h5>Zcash Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in payments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
|
||||
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Crypto)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
|
|
|
@ -327,7 +327,7 @@
|
|||
<td>@Model.TransactionSpeed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Fiat Due</th>
|
||||
<th>Total Amount Due</th>
|
||||
<td><span data-sensitive>@Model.Fiat</span></td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(Model.RefundEmail))
|
||||
|
@ -506,7 +506,8 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain">
|
||||
<h3 class="mb-3">Invoice Summary</h3>
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)" />
|
||||
|
||||
|
@ -638,20 +639,22 @@
|
|||
<h3 class="mb-0">Events</h3>
|
||||
<table class="table table-hover mt-3 mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var evt in Model.Events)
|
||||
{
|
||||
<tr class="text-@evt.GetCssClass()">
|
||||
<td>@evt.Timestamp.ToBrowserDate()</td>
|
||||
<td>@evt.Message</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var evt in Model.Events)
|
||||
{
|
||||
<tr class="text-@evt.GetCssClass()">
|
||||
<td>@evt.Timestamp.ToBrowserDate()</td>
|
||||
<td>@evt.Message</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
@section PageHeadContent
|
||||
{
|
||||
<style>
|
||||
.invoice-payments {
|
||||
padding-left: var(--btcpay-space-l);
|
||||
.invoice-details-row > td {
|
||||
padding: 1.5rem .5rem 0 2.65rem;
|
||||
}
|
||||
.dropdown > .btn {
|
||||
min-width: 7rem;
|
||||
|
|
|
@ -6,22 +6,41 @@
|
|||
.Where(entities => entities.Key != null);
|
||||
}
|
||||
|
||||
@if (invoice.Overpaid)
|
||||
{
|
||||
var usedPaymentMethods = invoice.CryptoPayments.Count(p => p.Paid != null);
|
||||
<p class="d-flex align-items-center gap-2 mb-3 text-warning">
|
||||
<vc:icon symbol="warning"/>
|
||||
This invoice got overpaid.
|
||||
@if (usedPaymentMethods > 1)
|
||||
{
|
||||
@("Each payment method shows the total excess amount.")
|
||||
}
|
||||
</p>
|
||||
}
|
||||
<div class="invoice-payments table-responsive mt-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-nowrap w-175px">Payment method</th>
|
||||
@if (Model.ShowAddress)
|
||||
{
|
||||
<th>Destination</th>
|
||||
}
|
||||
<th class="w-150px text-end">Rate</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
<th class="w-150px text-end">Due</th>
|
||||
@if (invoice.Overpaid)
|
||||
@if (invoice.HasRates)
|
||||
{
|
||||
<th class="w-150px text-end">Rate</th>
|
||||
}
|
||||
<th class="w-150px text-end">Total due</th>
|
||||
@if (invoice.StillDue)
|
||||
{
|
||||
<th class="w-150px text-end">Still due</th>
|
||||
}
|
||||
else if (invoice.Overpaid)
|
||||
{
|
||||
<th class="w-150px text-end">Overpaid</th>
|
||||
}
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -35,13 +54,39 @@
|
|||
<vc:truncate-center text="@payment.Address" classes="truncate-center-id" />
|
||||
</td>
|
||||
}
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Rate</span></td>
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Paid</span></td>
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Due</span></td>
|
||||
@if (invoice.Overpaid)
|
||||
@if (invoice.HasRates)
|
||||
{
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Overpaid</span></td>
|
||||
<td class="text-nowrap text-end">
|
||||
<span data-sensitive>@payment.Rate</span>
|
||||
</td>
|
||||
}
|
||||
<td class="text-nowrap text-end">
|
||||
<span data-sensitive>@payment.TotalDue</span>
|
||||
</td>
|
||||
@if (invoice.StillDue)
|
||||
{
|
||||
<td class="text-nowrap text-end">
|
||||
@if (payment.Due != null)
|
||||
{
|
||||
<span data-sensitive>@payment.Due</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else if (invoice.Overpaid)
|
||||
{
|
||||
<td class="text-nowrap text-end">
|
||||
@if (payment.Overpaid != null)
|
||||
{
|
||||
<span data-sensitive class="text-warning">@payment.Overpaid</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="text-nowrap text-end">
|
||||
@if (payment.Paid != null)
|
||||
{
|
||||
<span data-sensitive class="text-success">@payment.Paid</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
var details = payment.PaymentMethodRaw.GetPaymentMethodDetails();
|
||||
var name = details.GetAdditionalDataPartialName();
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th permission="@Policies.CanModifyStoreSettings">
|
||||
<input id="@Model.PayoutState-selectAllCheckbox" type="checkbox" class="form-check-input selectAll" data-payout-state="@Model.PayoutState.ToString()" />
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<a asp-action="PullPayments"
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
|
||||
<div id="WalletTransactions" class="table-responsive-md">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2rem;" class="only-for-js">
|
||||
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
|
||||
|
|
|
@ -76,7 +76,7 @@ hr.primary {
|
|||
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl-constrain {
|
||||
max-width: 800px;
|
||||
max-width: 984px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue