Make invoice payments view modular per payment handler (#991)

This commit is contained in:
Andrew Camilleri
2019-08-24 16:10:13 +02:00
committed by Nicolas Dorier
parent 7ea3312534
commit 664b920a39
8 changed files with 175 additions and 166 deletions

View File

@@ -72,7 +72,8 @@ namespace BTCPayServer.Controllers
StatusException = invoice.ExceptionStatus, StatusException = invoice.ExceptionStatus,
Events = invoice.Events, Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.PosData), PosData = PosDataParser.ParsePosData(invoice.PosData),
StatusMessage = StatusMessage StatusMessage = StatusMessage,
}; };
model.Addresses = invoice.HistoricalAddresses.Select(h => model.Addresses = invoice.HistoricalAddresses.Select(h =>
@@ -85,20 +86,21 @@ namespace BTCPayServer.Controllers
var details = InvoicePopulatePayments(invoice); var details = InvoicePopulatePayments(invoice);
model.CryptoPayments = details.CryptoPayments; model.CryptoPayments = details.CryptoPayments;
model.OnChainPayments = details.OnChainPayments; model.Payments = details.Payments;
model.OffChainPayments = details.OffChainPayments;
return View(model); return View(model);
} }
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice) private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{ {
var model = new InvoiceDetailsModel(); var model = new InvoiceDetailsModel();
model.Payments = invoice.GetPayments();
foreach (var data in invoice.GetPaymentMethods()) foreach (var data in invoice.GetPaymentMethods())
{ {
var accounting = data.Calculate(); var accounting = data.Calculate();
var paymentMethodId = data.GetId(); var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment(); var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethodId = paymentMethodId;
cryptoPayment.PaymentMethod = paymentMethodId.ToPrettyString(); cryptoPayment.PaymentMethod = paymentMethodId.ToPrettyString();
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
@@ -108,43 +110,6 @@ namespace BTCPayServer.Controllers
cryptoPayment.Rate = ExchangeRate(data); cryptoPayment.Rate = ExchangeRate(data);
model.CryptoPayments.Add(cryptoPayment); model.CryptoPayments.Add(cryptoPayment);
} }
foreach (var payment in invoice.GetPayments())
{
var paymentData = payment.GetCryptoPaymentData();
//TODO: abstract
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination();
int confirmationCount = onChainPaymentData.ConfirmationCount;
if (confirmationCount >= payment.Network.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (payment.Network.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
model.OnChainPayments.Add(m);
}
else
{
var lightningPaymentData = (LightningLikePaymentData)paymentData;
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
{
Crypto = payment.Network.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
});
}
}
return model; return model;
} }

View File

@@ -3,11 +3,32 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Models.InvoicingModels namespace BTCPayServer.Models.InvoicingModels
{ {
public class OnchainPaymentViewModel
{
public string Crypto { get; set; }
public string Confirmations { get; set; }
public BitcoinAddress DepositAddress { get; set; }
public string Amount { get; set; }
public string TransactionId { get; set; }
public DateTimeOffset ReceivedTime { get; set; }
public string TransactionLink { get; set; }
public bool Replaced { get; set; }
}
public class OffChainPaymentViewModel
{
public string Crypto { get; set; }
public string BOLT11 { get; set; }
}
public class InvoiceDetailsModel public class InvoiceDetailsModel
{ {
public class CryptoPayment public class CryptoPayment
@@ -19,6 +40,8 @@ namespace BTCPayServer.Models.InvoicingModels
public string Rate { get; internal set; } public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; } public string PaymentUrl { get; internal set; }
public string Overpaid { get; set; } public string Overpaid { get; set; }
[JsonIgnore]
public PaymentMethodId PaymentMethodId { get; set; }
} }
public class AddressModel public class AddressModel
{ {
@@ -26,39 +49,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string Destination { get; set; } public string Destination { get; set; }
public bool Current { get; set; } public bool Current { get; set; }
} }
public class Payment
{
public string Crypto { get; set; }
public string Confirmations
{
get; set;
}
public BitcoinAddress DepositAddress
{
get; set;
}
public string Amount
{
get; set;
}
public string TransactionId
{
get; set;
}
public DateTimeOffset ReceivedTime
{
get;
internal set;
}
public string TransactionLink
{
get;
set;
}
public bool Replaced { get; set; }
}
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
@@ -73,14 +64,6 @@ namespace BTCPayServer.Models.InvoicingModels
get; set; get; set;
} = new List<CryptoPayment>(); } = new List<CryptoPayment>();
public List<Payment> OnChainPayments { get; set; } = new List<Payment>();
public List<OffChainPayment> OffChainPayments { get; set; } = new List<OffChainPayment>();
public class OffChainPayment
{
public string Crypto { get; set; }
public string BOLT11 { get; set; }
}
public string State public string State
{ {
get; set; get; set;
@@ -145,5 +128,6 @@ namespace BTCPayServer.Models.InvoicingModels
public List<Data.InvoiceEventData> Events { get; internal set; } public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; } public string NotificationEmail { get; internal set; }
public Dictionary<string, object> PosData { get; set; } public Dictionary<string, object> PosData { get; set; }
public List<PaymentEntity> Payments { get; set; }
} }
} }

View File

@@ -56,5 +56,6 @@ namespace BTCPayServer.Payments
return null; return null;
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
} }
public override string InvoiceViewPaymentPartialName { get; } = "ViewBitcoinLikePaymentData";
} }
} }

View File

@@ -39,5 +39,6 @@ namespace BTCPayServer.Payments
{ {
return null; return null;
} }
public override string InvoiceViewPaymentPartialName { get; } = "ViewLightningLikePaymentData";
} }
} }

View File

@@ -61,5 +61,6 @@ namespace BTCPayServer.Payments
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value); public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId); public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
public abstract string InvoiceViewPaymentPartialName { get; }
} }
} }

View File

@@ -1,100 +1,49 @@
@model InvoiceDetailsModel @model InvoiceDetailsModel
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
<div class="row"> <div class="row">
<div class="col-md-12 invoice-payments"> <div class="col-md-12 invoice-payments">
<h3>Paid summary</h3> <h3>Paid summary</h3>
<table class="table table-sm table-responsive-md"> <table class="table table-sm table-responsive-md">
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th>Payment method</th> <th>Payment method</th>
<th>Address</th> <th>Address</th>
<th class="text-right">Rate</th> <th class="text-right">Rate</th>
<th class="text-right">Paid</th> <th class="text-right">Paid</th>
<th class="text-right">Due</th> <th class="text-right">Due</th>
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver) @if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
{ {
<th class="text-right">Overpaid</th> <th class="text-right">Overpaid</th>
} }
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var payment in Model.CryptoPayments) @foreach (var payment in Model.CryptoPayments)
{ {
<tr> <tr>
<td>@payment.PaymentMethod</td> <td>@payment.PaymentMethod</td>
<td title="@payment.Address"> <td title="@payment.Address">
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span> <span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
</td> </td>
<td class="text-right">@payment.Rate</td> <td class="text-right">@payment.Rate</td>
<td class="text-right">@payment.Paid</td> <td class="text-right">@payment.Paid</td>
<td class="text-right">@payment.Due</td> <td class="text-right">@payment.Due</td>
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver) @if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
{ {
<td class="text-right">@payment.Overpaid</td> <td class="text-right">@payment.Overpaid</td>
} }
</tr> </tr>
} }
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
@if (Model.OnChainPayments.Count > 0) @{
{ var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType);
<div class="row">
<div class="col-md-12 invoice-payments">
<h3>On-Chain payments</h3>
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Deposit address</th>
<th>Transaction Id</th>
<th class="text-right">Confirmations</th>
</tr>
</thead>
<tbody>
@foreach (var payment in Model.OnChainPayments)
{
<tr class="@(payment.Replaced ? "linethrough" : "")" >
<td>@payment.Crypto</td>
<td>@payment.DepositAddress</td>
<td>
<div class="wraptextAuto">
<a href="@payment.TransactionLink" target="_blank">
@payment.TransactionId
</a>
</div>
</td>
<td class="text-right">@payment.Confirmations</td>
</tr>
}
</tbody>
</table>
</div>
</div>
} }
@if (Model.OffChainPayments.Count > 0) @foreach (var paymentGroup in grouped)
{ {
<div class="row"> <partial name="@paymentGroup.Key.InvoiceViewPaymentPartialName" model="@paymentGroup.ToList()" />
<div class="col-md-12 invoice-payments">
<h3>Off-Chain payments</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th class="firstCol">Crypto</th>
<th>BOLT11</th>
</tr>
</thead>
<tbody>
@foreach (var payment in Model.OffChainPayments)
{
<tr>
<td>@payment.Crypto</td>
<td><div class="wraptextAuto">@payment.BOLT11</div></td>
</tr>
}
</tbody>
</table>
</div>
</div>
} }

View File

@@ -0,0 +1,66 @@
@using System.Globalization
@using BTCPayServer.Payments
@using BTCPayServer.Payments.Bitcoin
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == BitcoinPaymentType.Instance).Select(payment =>
{
var m = new OnchainPaymentViewModel();
var onChainPaymentData = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination();
int confirmationCount = onChainPaymentData.ConfirmationCount;
if (confirmationCount >= payment.Network.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (payment.Network.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
});
}
@if (onchainPayments.Any())
{
<div class="row">
<div class="col-md-12 invoice-payments">
<h3>On-Chain payments</h3>
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Deposit address</th>
<th>Transaction Id</th>
<th class="text-right">Confirmations</th>
</tr>
</thead>
<tbody>
@foreach (var payment in onchainPayments)
{
<tr class="@(payment.Replaced ? "linethrough" : "")" >
<td>@payment.Crypto</td>
<td>@payment.DepositAddress</td>
<td>
<div class="wraptextAuto">
<a href="@payment.TransactionLink" target="_blank">
@payment.TransactionId
</a>
</div>
</td>
<td class="text-right">@payment.Confirmations</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}

View File

@@ -0,0 +1,42 @@
@using BTCPayServer.Payments
@using BTCPayServer.Payments.Lightning
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{
var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == LightningPaymentType.Instance).Select(payment =>
{
var offChainPaymentData = payment.GetCryptoPaymentData() as LightningLikePaymentData;
return new OffChainPaymentViewModel()
{
Crypto = payment.Network.CryptoCode,
BOLT11 = offChainPaymentData.BOLT11
};
});
}
@if (offchainPayments.Any())
{
<div class="row">
<div class="col-md-12 invoice-payments">
<h3>Off-Chain payments</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th class="firstCol">Crypto</th>
<th>BOLT11</th>
</tr>
</thead>
<tbody>
@foreach (var payment in offchainPayments)
{
<tr>
<td>@payment.Crypto</td>
<td><div class="wraptextAuto">@payment.BOLT11</div></td>
</tr>
}
</tbody>
</table>
</div>
</div>
}