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();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
watcher.PollInterval = TimeSpan.FromMilliseconds(50);
watcher.PollInterval = TimeSpan.FromMilliseconds(500);
}
public BTCPayServerRuntime Runtime

View File

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

View File

@@ -20,6 +20,77 @@ namespace BTCPayServer.Controllers
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]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]

View File

@@ -37,6 +37,7 @@ using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
namespace BTCPayServer.Controllers
{
@@ -51,6 +52,7 @@ namespace BTCPayServer.Controllers
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
ExplorerClient _Explorer;
public InvoiceController(
Network network,
@@ -61,8 +63,10 @@ namespace BTCPayServer.Controllers
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
{
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_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);
JsonConvert.PopulateObject(str, dest);
}
public Money GetNetworkFee()
{
var item = Calculate();
return TxFee * item.TxCount;
}
}
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())
{
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
try
{
await ctx.SaveChangesAsync();
return true;
}
catch(DbUpdateException) { return false; }
}
}
@@ -207,12 +212,16 @@ namespace BTCPayServer.Servcices.Invoices
{
using(var context = _ContextFactory.CreateContext())
{
IQueryable<InvoiceData> query = context
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if(!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if(!string.IsNullOrEmpty(queryObject.StoreId))
{
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
@@ -380,5 +389,10 @@ namespace BTCPayServer.Servcices.Invoices
{
get; set;
}
public string InvoiceId
{
get;
set;
}
}
}

View File

@@ -29,6 +29,8 @@ namespace BTCPayServer.Servcices.Invoices
BTCPayWallet wallet,
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));
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
@@ -36,6 +38,11 @@ namespace BTCPayServer.Servcices.Invoices
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
}
public bool LongPollingMode
{
get; set;
}
public async Task NotifyReceived(Script scriptPubKey)
{
var invoice = await _Wallet.GetInvoiceId(scriptPubKey);
@@ -52,7 +59,6 @@ namespace BTCPayServer.Servcices.Invoices
private async Task UpdateInvoice(string invoiceId)
{
Logs.PayServer.LogInformation("Updating invoice " + invoiceId);
UTXOChanges changes = null;
while(true)
{
@@ -75,7 +81,7 @@ namespace BTCPayServer.Servcices.Invoices
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);
break;
}
@@ -98,23 +104,18 @@ namespace BTCPayServer.Servcices.Invoices
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
{
if(invoice.Status == "invalid")
{
return (false, changes);
}
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;
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);
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 invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
@@ -123,8 +124,6 @@ namespace BTCPayServer.Servcices.Invoices
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
.ToArray();
shouldWait = false; //should not wait, Sync is blocking call
List<Coin> receivedCoins = new List<Coin>();
foreach(var received in utxos)
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
@@ -218,23 +217,30 @@ namespace BTCPayServer.Servcices.Invoices
}
}
shouldWait = shouldWait && !needSave;
if(shouldWait)
{
await Task.Delay(PollInterval, _Cts.Token).ConfigureAwait(false);
}
return (needSave, changes);
}
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get; set;
} = TimeSpan.FromSeconds(10);
public async Task WatchAsync(string invoiceId)
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if(_UpdatePendingInvoices != null)
{
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
}
}
}
public async Task WatchAsync(string invoiceId, bool singleShot = false)
{
if(!singleShot)
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
_WatchRequests.Add(invoiceId);
}
@@ -264,7 +270,7 @@ namespace BTCPayServer.Servcices.Invoices
{
_WatchRequests.Add(pending);
}
}, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
}, null, 0, (int)PollInterval.TotalMilliseconds);
return Task.CompletedTask;
}

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.Status</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>
}
</tbody>