Invoice Details: Improve payments list and print view (#4817)

Closes #4729.

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n
2023-04-04 03:59:14 +02:00
committed by GitHub
parent 2bd1842da1
commit 11f05285a1
6 changed files with 140 additions and 117 deletions

View File

@@ -20,6 +20,7 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
@@ -256,7 +257,7 @@ namespace BTCPayServer.Controllers
return paymentData switch return paymentData switch
{ {
BitcoinLikePaymentData b => b.Outpoint.ToString(), BitcoinLikePaymentData b => b.Outpoint.ToString(),
LightningPaymentData l => l.Preimage, LightningLikePaymentData l => l.Preimage?.ToString(),
_ => null _ => null
}; };
} }

View File

@@ -23,8 +23,8 @@ namespace BTCPayServer.Models.InvoicingModels
public bool Replaced { get; set; } public bool Replaced { get; set; }
public BitcoinLikePaymentData CryptoPaymentData { get; set; } public BitcoinLikePaymentData CryptoPaymentData { get; set; }
public string AdditionalInformation { get; set; } public string AdditionalInformation { get; set; }
public decimal NetworkFee { get; set; } public decimal NetworkFee { get; set; }
public string PaymentProof { get; set; }
} }
public class OffChainPaymentViewModel public class OffChainPaymentViewModel
@@ -32,6 +32,8 @@ namespace BTCPayServer.Models.InvoicingModels
public string Crypto { get; set; } public string Crypto { get; set; }
public string BOLT11 { get; set; } public string BOLT11 { get; set; }
public PaymentType Type { get; set; } public PaymentType Type { get; set; }
public string Amount { get; set; }
public string PaymentProof { get; set; }
} }
public class InvoiceDetailsModel public class InvoiceDetailsModel

View File

@@ -4,63 +4,67 @@
@using BTCPayServer.Services @using BTCPayServer.Services
@inject DisplayFormatter DisplayFormatter @inject DisplayFormatter DisplayFormatter
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity> @model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{ @{
PayjoinInformation payjoinIformation = null; PayjoinInformation payjoinInformation = null;
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance).Select(payment => var payments = Model
{ .Where(entity => entity.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance)
var m = new OnchainPaymentViewModel(); .Select(payment =>
var onChainPaymentData = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
if (onChainPaymentData is null)
{ {
return null; var m = new OnchainPaymentViewModel();
} var onChainPaymentData = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
m.Crypto = payment.GetPaymentMethodId().CryptoCode; if (onChainPaymentData is null)
m.DepositAddress = onChainPaymentData.GetDestination(); {
return null;
}
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination();
long confirmationCount = onChainPaymentData.ConfirmationCount; var confirmationCount = onChainPaymentData.ConfirmationCount;
var network = payment.Network as BTCPayNetwork; var network = payment.Network as BTCPayNetwork;
if (confirmationCount >= network.MaxTrackedConfirmation) if (confirmationCount >= network.MaxTrackedConfirmation)
{ {
m.Confirmations = "At least " + (network.MaxTrackedConfirmation); m.Confirmations = "At least " + (network.MaxTrackedConfirmation);
} }
else else
{ {
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture); m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
} }
if (onChainPaymentData?.PayjoinInformation is PayjoinInformation pj) if (onChainPaymentData.PayjoinInformation is PayjoinInformation pj)
{ {
payjoinIformation = pj; payjoinInformation = pj;
m.AdditionalInformation = "Original transaction"; m.AdditionalInformation = "Original transaction";
} }
if (payjoinIformation is PayjoinInformation && if (payjoinInformation is PayjoinInformation &&
payjoinIformation.CoinjoinTransactionHash == onChainPaymentData?.Outpoint.Hash) payjoinInformation.CoinjoinTransactionHash == onChainPaymentData?.Outpoint.Hash)
{ {
m.AdditionalInformation = "Payjoin transaction"; m.AdditionalInformation = "Payjoin transaction";
} }
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString(); m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime; m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId); m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted; m.Replaced = !payment.Accounted;
m.CryptoPaymentData = onChainPaymentData; m.CryptoPaymentData = onChainPaymentData;
m.NetworkFee = payment.NetworkFee; m.NetworkFee = payment.NetworkFee;
return m; m.PaymentProof = onChainPaymentData.Outpoint.ToString();
}).Where(model => model != null); return m;
})
.Where(model => model != null)
.ToList();
} }
@if (onchainPayments.Any()) @if (payments.Any())
{ {
var hasNetworkFee = onchainPayments.Sum(a => a.NetworkFee) > 0; var hasNetworkFee = payments.Sum(a => a.NetworkFee) > 0;
<section> <section>
<h5>On-Chain Payments</h5> <h5>On-Chain Payments</h5>
<table class="table table-hover mt-3 mb-0"> <div class="invoice-payments table-responsive mt-0">
<thead class="thead-inverse"> <table class="table table-hover mb-0">
<thead class="thead-inverse">
<tr> <tr>
<th>Crypto</th> <th class="w-75px">Crypto</th>
<th>Index</th> <th class="w-100px">Index</th>
<th class="text-nowrap">Deposit address</th> <th class="w-175px">Destination</th>
<th>Transaction Id</th> <th class="text-nowrap">Payment Proof</th>
<th class="text-end">Amount</th>
@if (hasNetworkFee) @if (hasNetworkFee)
{ {
<th class="text-end"> <th class="text-end">
@@ -71,24 +75,26 @@
</th> </th>
} }
<th class="text-end">Confirmations</th> <th class="text-end">Confirmations</th>
<th class="w-150px text-end">Amount</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var payment in onchainPayments) @foreach (var payment in payments)
{ {
<tr style="@(payment.Replaced ? "text-decoration: line-through" : "")"> <tr style="@(payment.Replaced ? "text-decoration: line-through" : "")">
<td>@payment.Crypto</td> <td>@payment.Crypto</td>
<td>@(payment.CryptoPaymentData.KeyPath?.ToString()?? "Unknown")</td> <td>@(payment.CryptoPaymentData.KeyPath?.ToString()?? "Unknown")</td>
<td> <td>
<div class="text-truncate" style="max-width:300px;" data-bs-toggle="tooltip" title="@payment.DepositAddress">@payment.DepositAddress</div> <vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" />
</td> </td>
<td> <td>
<div class="text-truncate" style="max-width:200px;" data-bs-toggle="tooltip" title="@payment.TransactionId"> <vc:truncate-center text="@payment.PaymentProof" link="@payment.TransactionLink" classes="truncate-center-id" />
<a href="@payment.TransactionLink" target="_blank" rel="noreferrer noopener">
@payment.TransactionId
</a>
</div>
</td> </td>
@if (hasNetworkFee)
{
<td class="text-end text-nowrap">@payment.NetworkFee</td>
}
<td class="text-end">@payment.Confirmations</td>
<td class="payment-value text-end text-nowrap"> <td class="payment-value text-end text-nowrap">
@DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto) @DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto)
@if (!string.IsNullOrEmpty(payment.AdditionalInformation)) @if (!string.IsNullOrEmpty(payment.AdditionalInformation))
@@ -96,14 +102,10 @@
<div>(@payment.AdditionalInformation)</div> <div>(@payment.AdditionalInformation)</div>
} }
</td> </td>
@if (hasNetworkFee)
{
<td class="text-end text-nowrap">@payment.NetworkFee</td>
}
<td class="text-end">@payment.Confirmations</td>
</tr> </tr>
} }
</tbody> </tbody>
</table> </table>
</div>
</section> </section>
} }

View File

@@ -1,47 +1,63 @@
@using BTCPayServer.Payments @using BTCPayServer.Payments
@using BTCPayServer.Payments.Lightning @using BTCPayServer.Payments.Lightning
@using BTCPayServer.Services
@using BTCPayServer.Components.TruncateCenter
@using BTCPayServer.Lightning
@inject DisplayFormatter DisplayFormatter
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity> @model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{ @{
var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId()?.PaymentType == LightningPaymentType.Instance || entity.GetPaymentMethodId()?.PaymentType == LNURLPayPaymentType.Instance).Select(payment => var payments = Model
{ .Where(entity => entity.GetPaymentMethodId()?.PaymentType == LightningPaymentType.Instance ||
var offChainPaymentData = payment.GetCryptoPaymentData() as LightningLikePaymentData; entity.GetPaymentMethodId()?.PaymentType == LNURLPayPaymentType.Instance)
if (offChainPaymentData is null) .Select(payment => payment.GetCryptoPaymentData() is LightningLikePaymentData offChainPaymentData
{ ? new OffChainPaymentViewModel
return null; {
} Crypto = payment.Network.CryptoCode,
return new OffChainPaymentViewModel BOLT11 = offChainPaymentData.BOLT11,
{ Type = payment.GetCryptoPaymentData().GetPaymentType(),
Crypto = payment.Network.CryptoCode, PaymentProof = offChainPaymentData.Preimage?.ToString(),
BOLT11 = offChainPaymentData.BOLT11, Amount = DisplayFormatter.Currency(offChainPaymentData.Amount.ToDecimal(LightMoneyUnit.BTC), payment.Network.CryptoCode)
Type = payment.GetCryptoPaymentData().GetPaymentType() }
}; : null)
}).Where(model => model != null); .Where(model => model != null)
.ToList();
} }
@if (offchainPayments.Any()) @if (payments.Any())
{ {
<section> <section>
<h5>Off-Chain Payments</h5> <h5>Off-Chain Payments</h5>
<table class="table table-hover"> <div class="invoice-payments table-responsive mt-0">
<thead class="thead-inverse"> <table class="table table-hover mb-0">
<tr> <thead class="thead-inverse">
<th class="w-150px">Crypto</th>
<th class="w-150px">Type</th>
<th>BOLT11</th>
</tr>
</thead>
<tbody>
@foreach (var payment in offchainPayments)
{
<tr> <tr>
<td>@payment.Crypto</td> <th class="w-75px">Crypto</th>
<td>@payment.Type.ToPrettyString()</td> <th class="w-100px">Type</th>
<td class="text-break">@payment.BOLT11</td> <th class="w-175px">Destination</th>
<th class="text-nowrap">Payment Proof</th>
<th class="w-150px text-end">Amount</th>
</tr> </tr>
} </thead>
</tbody> <tbody>
</table> @foreach (var payment in payments)
{
<tr>
<td>@payment.Crypto</td>
<td>@payment.Type.ToPrettyString()</td>
<td>
<vc:truncate-center text="@payment.BOLT11" classes="truncate-center-id" />
</td>
<td>
<vc:truncate-center text="@payment.PaymentProof" classes="truncate-center-id" />
</td>
<td class="payment-value text-end text-nowrap">
@payment.Amount
</td>
</tr>
}
</tbody>
</table>
</div>
</section> </section>
} }

View File

@@ -379,7 +379,7 @@
</div> </div>
</div> </div>
<h3 class="mb-0">Invoice Summary</h3> <h3 class="mb-3">Invoice Summary</h3>
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)"/> <partial name="ListInvoicesPaymentsPartial" model="(Model, true)"/>
@if (Model.Deliveries.Any()) @if (Model.Deliveries.Any())

View File

@@ -1,21 +1,26 @@
@model (InvoiceDetailsModel Invoice, bool ShowAddress) @model (InvoiceDetailsModel Invoice, bool ShowAddress)
@{ var invoice = Model.Invoice; } @{
var invoice = Model.Invoice;
var grouped = invoice.Payments
.GroupBy(payment => payment.GetPaymentMethodId()?.PaymentType)
.Where(entities => entities.Key != null);
}
<div class="invoice-payments table-responsive"> <div class="invoice-payments table-responsive mt-0">
<table class="table table-hover mt-3 mb-4"> <table class="table table-hover mb-0">
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th class="text-nowrap">Payment method</th> <th class="text-nowrap w-175px">Payment method</th>
@if (Model.ShowAddress) @if (Model.ShowAddress)
{ {
<th>Address</th> <th>Destination</th>
} }
<th class="text-end">Rate</th> <th class="w-150px text-end">Rate</th>
<th class="text-end">Paid</th> <th class="w-150px text-end">Paid</th>
<th class="text-end">Due</th> <th class="w-150px text-end">Due</th>
@if (invoice.Overpaid) @if (invoice.Overpaid)
{ {
<th class="text-end">Overpaid</th> <th class="w-150px text-end">Overpaid</th>
} }
</tr> </tr>
</thead> </thead>
@@ -27,7 +32,7 @@
@if (Model.ShowAddress) @if (Model.ShowAddress)
{ {
<td title="@payment.Address"> <td title="@payment.Address">
<div class="text-truncate" style="max-width:400px" data-bs-toggle="tooltip" title="@payment.Address">@payment.Address</div> <vc:truncate-center text="@payment.Address" classes="truncate-center-id" />
</td> </td>
} }
<td class="text-nowrap text-end">@payment.Rate</td> <td class="text-nowrap text-end">@payment.Rate</td>
@@ -47,11 +52,8 @@
} }
</tbody> </tbody>
</table> </table>
@{
var grouped = invoice.Payments.GroupBy(payment => payment.GetPaymentMethodId()?.PaymentType).Where(entities => entities.Key != null);
}
@foreach (var paymentGroup in grouped)
{
<partial name="@paymentGroup.Key.InvoiceViewPaymentPartialName" model="@paymentGroup.ToList()" />
}
</div> </div>
@foreach (var paymentGroup in grouped)
{
<partial name="@paymentGroup.Key.InvoiceViewPaymentPartialName" model="@paymentGroup.ToList()" />
}