Add page for viewing the Invoice details

This commit is contained in:
nicolas.dorier
2017-10-13 00:25:45 +09:00
parent d469084596
commit 016db76306
10 changed files with 473 additions and 39 deletions

View File

@@ -108,7 +108,7 @@ namespace BTCPayServer.Tests
_Host.Start(); _Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime)); Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher)); var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
watcher.PollInterval = TimeSpan.FromMilliseconds(50); watcher.PollInterval = TimeSpan.FromMilliseconds(500);
} }
public BTCPayServerRuntime Runtime public BTCPayServerRuntime Runtime

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.8</Version> <Version>1.0.0.9</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Build\dockerfiles\**" /> <Compile Remove="Build\dockerfiles\**" />

View File

@@ -17,8 +17,79 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public partial class InvoiceController public partial class InvoiceController
{ {
[HttpPost]
[Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId, string command)
{
if(command == "refresh")
{
await _Watcher.WatchAsync(invoiceId, true);
}
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
return RedirectToAction(nameof(Invoice), new
{
invoiceId = invoiceId
});
}
[HttpGet]
[Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId
})).FirstOrDefault();
if(invoice == null)
return NotFound();
var dto = invoice.EntityToDTO();
var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Rate = invoice.Rate,
Fiat = dto.Price + " " + dto.Currency,
BTC = invoice.GetTotalCryptoDue().ToString() + " BTC",
BTCDue = invoice.GetCryptoDue().ToString() + " BTC",
BTCPaid = invoice.GetTotalPaid().ToString() + " BTC",
NetworkFee = invoice.GetNetworkFee().ToString() + " BTC",
NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation,
BitcoinAddress = invoice.DepositAddress,
PaymentUrl = dto.PaymentUrls.BIP72
};
var payments = invoice
.Payments
.Select(async payment =>
{
var m = new InvoiceDetailsModel.Payment();
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/{m.TransactionId}";
return m;
})
.ToArray();
await Task.WhenAll(payments);
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
model.StatusMessage = StatusMessage;
return View(model);
}
[HttpGet] [HttpGet]
[Route("i/{invoiceId}")] [Route("i/{invoiceId}")]
@@ -168,14 +239,14 @@ namespace BTCPayServer.Controllers
FullNotifications = true, FullNotifications = true,
BuyerEmail = model.BuyerEmail, BuyerEmail = model.BuyerEmail,
}, store); }, store);
StatusMessage = $"Invoice {result.Data.Id} just created!"; StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices)); return RedirectToAction(nameof(ListInvoices));
} }
private async Task<SelectList> GetStores(string userId, string storeId = null) private async Task<SelectList> GetStores(string userId, string storeId = null)
{ {
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
} }
[HttpPost] [HttpPost]

View File

@@ -37,6 +37,7 @@ using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using NBXplorer;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -51,6 +52,7 @@ namespace BTCPayServer.Controllers
Network _Network; Network _Network;
UserManager<ApplicationUser> _UserManager; UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider; IFeeProvider _FeeProvider;
ExplorerClient _Explorer;
public InvoiceController( public InvoiceController(
Network network, Network network,
@@ -61,8 +63,10 @@ namespace BTCPayServer.Controllers
IRateProvider rateProvider, IRateProvider rateProvider,
StoreRepository storeRepository, StoreRepository storeRepository,
InvoiceWatcher watcher, InvoiceWatcher watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider) IFeeProvider feeProvider)
{ {
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network)); _Network = network ?? throw new ArgumentNullException(nameof(network));
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository)); _TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Servcices.Invoices;
using NBitcoin;
namespace BTCPayServer.Models.InvoicingModels
{
public class InvoiceDetailsModel
{
public class Payment
{
public int 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 string StatusMessage
{
get; set;
}
public String Id
{
get; set;
}
public List<Payment> Payments
{
get; set;
} = new List<Payment>();
public string Status
{
get; set;
}
public DateTimeOffset CreatedDate
{
get; set;
}
public DateTimeOffset ExpirationDate
{
get; set;
}
public string OrderId
{
get; set;
}
public string RefundEmail
{
get;
set;
}
public BuyerInformation BuyerInformation
{
get;
set;
}
public object StoreName
{
get;
internal set;
}
public string StoreLink
{
get;
set;
}
public double Rate
{
get;
internal set;
}
public string NotificationUrl
{
get;
internal set;
}
public string Fiat
{
get;
set;
}
public string BTC
{
get;
set;
}
public string BTCDue
{
get;
set;
}
public string BTCPaid
{
get;
internal set;
}
public String NetworkFee
{
get;
internal set;
}
public ProductInformation ProductInformation
{
get;
internal set;
}
public BitcoinAddress BitcoinAddress
{
get;
internal set;
}
public string PaymentUrl
{
get;
set;
}
}
}

View File

@@ -302,6 +302,12 @@ namespace BTCPayServer.Servcices.Invoices
var str = JsonConvert.SerializeObject(from); var str = JsonConvert.SerializeObject(from);
JsonConvert.PopulateObject(str, dest); JsonConvert.PopulateObject(str, dest);
} }
public Money GetNetworkFee()
{
var item = Calculate();
return TxFee * item.TxCount;
}
} }
public class PaymentEntity public class PaymentEntity

View File

@@ -65,12 +65,17 @@ namespace BTCPayServer.Servcices.Invoices
} }
} }
public async Task RemovePendingInvoice(string invoiceId) public async Task<bool> RemovePendingInvoice(string invoiceId)
{ {
using(var ctx = _ContextFactory.CreateContext()) using(var ctx = _ContextFactory.CreateContext())
{ {
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
await ctx.SaveChangesAsync(); try
{
await ctx.SaveChangesAsync();
return true;
}
catch(DbUpdateException) { return false; }
} }
} }
@@ -207,12 +212,16 @@ namespace BTCPayServer.Servcices.Invoices
{ {
using(var context = _ContextFactory.CreateContext()) using(var context = _ContextFactory.CreateContext())
{ {
IQueryable<InvoiceData> query = context IQueryable<InvoiceData> query = context
.Invoices .Invoices
.Include(o => o.Payments) .Include(o => o.Payments)
.Include(o => o.RefundAddresses); .Include(o => o.RefundAddresses);
if(!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if(!string.IsNullOrEmpty(queryObject.StoreId)) if(!string.IsNullOrEmpty(queryObject.StoreId))
{ {
query = query.Where(i => i.StoreDataId == queryObject.StoreId); query = query.Where(i => i.StoreDataId == queryObject.StoreId);
@@ -380,5 +389,10 @@ namespace BTCPayServer.Servcices.Invoices
{ {
get; set; get; set;
} }
public string InvoiceId
{
get;
set;
}
} }
} }

View File

@@ -24,11 +24,13 @@ namespace BTCPayServer.Servcices.Invoices
InvoiceNotificationManager _NotificationManager; InvoiceNotificationManager _NotificationManager;
BTCPayWallet _Wallet; BTCPayWallet _Wallet;
public InvoiceWatcher(ExplorerClient explorerClient, public InvoiceWatcher(ExplorerClient explorerClient,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
BTCPayWallet wallet, BTCPayWallet wallet,
InvoiceNotificationManager notificationManager) InvoiceNotificationManager notificationManager)
{ {
LongPollingMode = explorerClient.Network == Network.RegTest;
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network); _DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
@@ -36,6 +38,11 @@ namespace BTCPayServer.Servcices.Invoices
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager)); _NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
} }
public bool LongPollingMode
{
get; set;
}
public async Task NotifyReceived(Script scriptPubKey) public async Task NotifyReceived(Script scriptPubKey)
{ {
var invoice = await _Wallet.GetInvoiceId(scriptPubKey); var invoice = await _Wallet.GetInvoiceId(scriptPubKey);
@@ -52,7 +59,6 @@ namespace BTCPayServer.Servcices.Invoices
private async Task UpdateInvoice(string invoiceId) private async Task UpdateInvoice(string invoiceId)
{ {
Logs.PayServer.LogInformation("Updating invoice " + invoiceId);
UTXOChanges changes = null; UTXOChanges changes = null;
while(true) while(true)
{ {
@@ -75,8 +81,8 @@ namespace BTCPayServer.Servcices.Invoices
if(invoice.Status == "complete" || invoice.Status == "invalid") if(invoice.Status == "complete" || invoice.Status == "invalid")
{ {
await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false); if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break; break;
} }
@@ -97,34 +103,27 @@ namespace BTCPayServer.Servcices.Invoices
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice) private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
{ {
if(invoice.Status == "invalid")
{
return (false, changes);
}
bool needSave = false; bool needSave = false;
bool shouldWait = true;
if(invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial")) if(invoice.Status != "invalid" && invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial"))
{ {
needSave = true; needSave = true;
invoice.Status = "invalid"; invoice.Status = "invalid";
} }
if(invoice.Status == "new" || invoice.Status == "paidPartial") if(invoice.Status == "invalid" || invoice.Status == "new" || invoice.Status == "paidPartial")
{ {
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
changes = await _ExplorerClient.SyncAsync(strategy, changes, true, _Cts.Token).ConfigureAwait(false); changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray(); var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
utxos = utxos =
utxos utxos
.Where((u,i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) .Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
.ToArray(); .ToArray();
shouldWait = false; //should not wait, Sync is blocking call
List<Coin> receivedCoins = new List<Coin>(); List<Coin> receivedCoins = new List<Coin>();
foreach(var received in utxos) foreach(var received in utxos)
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey) if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
@@ -218,24 +217,31 @@ namespace BTCPayServer.Servcices.Invoices
} }
} }
shouldWait = shouldWait && !needSave;
if(shouldWait)
{
await Task.Delay(PollInterval, _Cts.Token).ConfigureAwait(false);
}
return (needSave, changes); return (needSave, changes);
} }
TimeSpan _PollInterval;
public TimeSpan PollInterval public TimeSpan PollInterval
{ {
get; set; get
} = TimeSpan.FromSeconds(10); {
return _PollInterval;
}
set
{
_PollInterval = value;
if(_UpdatePendingInvoices != null)
{
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
}
}
}
public async Task WatchAsync(string invoiceId) public async Task WatchAsync(string invoiceId, bool singleShot = false)
{ {
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false); if(!singleShot)
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
_WatchRequests.Add(invoiceId); _WatchRequests.Add(invoiceId);
} }
@@ -264,7 +270,7 @@ namespace BTCPayServer.Servcices.Invoices
{ {
_WatchRequests.Add(pending); _WatchRequests.Add(pending);
} }
}, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds); }, null, 0, (int)PollInterval.TotalMilliseconds);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -279,7 +285,7 @@ namespace BTCPayServer.Servcices.Invoices
var localItem = item; var localItem = item;
// If the invoice is already updating, ignore // If the invoice is already updating, ignore
Lazy<Task> updateInvoice =new Lazy<Task>(() => UpdateInvoice(localItem), false); Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false);
if(updating.TryAdd(item, updateInvoice)) if(updating.TryAdd(item, updateInvoice))
{ {
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice)); updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));

View File

@@ -0,0 +1,187 @@
@model InvoiceDetailsModel
@{
ViewData["Title"] = "Invoice " + Model.Id;
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", Model.StatusMessage)
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Invoice details</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<table class="table">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
</tr>
<tr>
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.CreatedDate</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>
</tr>
<tr>
<th>Refund email</th>
<td>@Model.RefundEmail</td>
</tr>
<tr>
<th>Order Id</th>
<td>@Model.OrderId</td>
</tr>
<tr>
<th>Rate</th>
<td>@Model.Rate</td>
</tr>
<tr>
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Network Fee</th>
<td>@Model.NetworkFee</td>
</tr>
<tr>
<th>Total crypto due</th>
<td>@Model.BTC</td>
</tr>
<tr>
<th>Crypto due</th>
<td>@Model.BTCDue</td>
</tr>
<tr>
<th>Crypto paid</th>
<td>@Model.BTCPaid</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Payment address</th>
<td>@Model.BitcoinAddress</td>
</tr>
<tr>
<th>Payment Url</th>
<td><a href="@Model.PaymentUrl">@Model.PaymentUrl</a></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table">
<tr>
<th>Name
<th>
<td>@Model.BuyerInformation.BuyerName</td>
</tr>
<tr>
<th>Email</th>
<td>@Model.BuyerInformation.BuyerEmail</td>
</tr>
<tr>
<th>Phone</th>
<td>@Model.BuyerInformation.BuyerPhone</td>
</tr>
<tr>
<th>Address 1</th>
<td>@Model.BuyerInformation.BuyerAddress1</td>
</tr>
<tr>
<th>Address 2</th>
<td>@Model.BuyerInformation.BuyerAddress2</td>
</tr>
<tr>
<th>City</th>
<td>@Model.BuyerInformation.BuyerCity</td>
</tr>
<tr>
<th>State</th>
<td>@Model.BuyerInformation.BuyerState</td>
</tr>
<tr>
<th>Country</th>
<td>@Model.BuyerInformation.BuyerCountry</td>
</tr>
<tr>
<th>Zip</th>
<td>@Model.BuyerInformation.BuyerZip</td>
</tr>
</table>
<h3>Product information</h3>
<table class="table">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
</tr>
<tr>
<th>Item Description</th>
<td>@Model.ProductInformation.ItemDesc</td>
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Payments</h3>
<div class="form-group">
<form asp-action="Invoice" method="post">
<button type="submit" name="command" class="btn btn-success" value="refresh" title="Refresh State">
Refresh state
</button>
</form>
</div>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Deposit address</th>
<th>Transaction Id</th>
<th>Confirmations</th>
</tr>
</thead>
<tbody>
@foreach(var payment in Model.Payments)
{
<tr>
<td>@payment.ReceivedTime</td>
<td>@payment.DepositAddress</td>
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
<td>@payment.Confirmations</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@@ -50,7 +50,7 @@
<td>@invoice.InvoiceId</td> <td>@invoice.InvoiceId</td>
<td>@invoice.Status</td> <td>@invoice.Status</td>
<td>@invoice.AmountCurrency</td> <td>@invoice.AmountCurrency</td>
<td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a></td> <td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> - <a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a></td>
</tr> </tr>
} }
</tbody> </tbody>