Refactor how invoice payments are computed

This commit is contained in:
nicolas.dorier
2017-12-21 18:01:26 +09:00
parent a37fdde214
commit a863812f90
6 changed files with 124 additions and 59 deletions

View File

@@ -47,28 +47,34 @@ namespace BTCPayServer.Tests
entity.Payments = new System.Collections.Generic.List<PaymentEntity>(); entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 }; entity.ProductInformation = new ProductInformation() { Price = 5000 };
Assert.Equal(Money.Coins(1.1m), cryptoData.GetCryptoDue()); var accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(1.1m), cryptoData.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 //Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), cryptoData.GetCryptoDue()); Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), cryptoData.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
Assert.Equal(Money.Coins(0.6m), cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue()); accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
Assert.Equal(Money.Zero, cryptoData.GetCryptoDue()); accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue()); Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
Assert.Equal(Money.Zero, cryptoData.GetCryptoDue()); accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue()); Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
#pragma warning restore CS0618 #pragma warning restore CS0618
} }

View File

@@ -62,6 +62,8 @@ namespace BTCPayServer.Controllers
var dto = invoice.EntityToDTO(_NetworkProvider); var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase)); var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
var store = await _StoreRepository.FindStore(invoice.StoreId); var store = await _StoreRepository.FindStore(invoice.StoreId);
var accounting = cryptoData.Calculate();
InvoiceDetailsModel model = new InvoiceDetailsModel() InvoiceDetailsModel model = new InvoiceDetailsModel()
{ {
StoreName = store.StoreName, StoreName = store.StoreName,
@@ -75,10 +77,10 @@ namespace BTCPayServer.Controllers
BuyerInformation = invoice.BuyerInformation, BuyerInformation = invoice.BuyerInformation,
Rate = cryptoData.Rate, Rate = cryptoData.Rate,
Fiat = dto.Price + " " + dto.Currency, Fiat = dto.Price + " " + dto.Currency,
BTC = cryptoData.GetTotalCryptoDue().ToString() + $" {network.CryptoCode}", BTC = accounting.TotalDue.ToString() + $" {network.CryptoCode}",
BTCDue = cryptoData.GetCryptoDue().ToString() + $" {network.CryptoCode}", BTCDue = accounting.Due.ToString() + $" {network.CryptoCode}",
BTCPaid = cryptoData.GetTotalPaid().ToString() + $" {network.CryptoCode}", BTCPaid = accounting.Paid.ToString() + $" {network.CryptoCode}",
NetworkFee = cryptoData.GetNetworkFee().ToString() + $" {network.CryptoCode}", NetworkFee = accounting.NetworkFee.ToString() + $" {network.CryptoCode}",
NotificationUrl = invoice.NotificationURL, NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation, ProductInformation = invoice.ProductInformation,
BitcoinAddress = BitcoinAddress.Create(cryptoInfo.Address, network.NBitcoinNetwork), BitcoinAddress = BitcoinAddress.Create(cryptoInfo.Address, network.NBitcoinNetwork),
@@ -137,15 +139,16 @@ namespace BTCPayServer.Controllers
var dto = invoice.EntityToDTO(_NetworkProvider); var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode); var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
var currency = invoice.ProductInformation.Currency; var currency = invoice.ProductInformation.Currency;
var accounting = cryptoData.Calculate();
var model = new PaymentModel() var model = new PaymentModel()
{ {
ServerUrl = HttpContext.Request.GetAbsoluteRoot(), ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId, OrderId = invoice.OrderId,
InvoiceId = invoice.Id, InvoiceId = invoice.Id,
BtcAddress = cryptoData.DepositAddress, BtcAddress = cryptoData.DepositAddress,
BtcAmount = (cryptoData.GetTotalCryptoDue() - cryptoData.TxFee).ToString(), BtcAmount = (accounting.TotalDue - cryptoData.TxFee).ToString(),
BtcTotalDue = cryptoData.GetTotalCryptoDue().ToString(), BtcTotalDue = accounting.TotalDue.ToString(),
BtcDue = cryptoData.GetCryptoDue().ToString(), BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail, CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
@@ -155,8 +158,8 @@ namespace BTCPayServer.Controllers
StoreName = store.StoreName, StoreName = store.StoreName,
TxFees = cryptoData.TxFee.ToString(), TxFees = cryptoData.TxFee.ToString(),
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP72, InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP72,
TxCount = cryptoData.GetTxCount(), TxCount = accounting.TxCount,
BtcPaid = cryptoData.GetTotalPaid().ToString(), BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status Status = invoice.Status
}; };

View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json; using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -66,18 +67,49 @@ namespace BTCPayServer.Models
} }
//"btcDue":"0.001160" //"btcDue":"0.001160"
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
[JsonProperty("due")] [JsonProperty("due")]
public string Due public string Due
{ {
get; set; get; set;
} }
[JsonProperty("paymentUrls")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls public NBitpayClient.InvoicePaymentUrls PaymentUrls
{ {
get; set; get; set;
} }
[JsonProperty("address")]
public string Address { get; set; } public string Address { get; set; }
[JsonProperty("url")]
public string Url { get; set; } public string Url { get; set; }
/// <summary>
/// Total amount of this invoice
/// </summary>
[JsonProperty("totalDue")]
public string TotalDue { get; set; }
/// <summary>
/// Total amount of network fee to pay to the invoice
/// </summary>
[JsonProperty("networkFee")]
public string NetworkFee { get; set; }
/// <summary>
/// Number of transactions required to pay
/// </summary>
[JsonProperty("txCount")]
public int TxCount { get; set; }
/// <summary>
/// Total amount of the invoice paid in this crypto
/// </summary>
[JsonProperty("cryptoPaid")]
public Money CryptoPaid { get; set; }
} }
//{"facade":"pos/invoice","data":{,}} //{"facade":"pos/invoice","data":{,}}

View File

@@ -255,13 +255,19 @@ namespace BTCPayServer.Services.Invoices
dto.CryptoInfo = new List<InvoiceCryptoInfo>(); dto.CryptoInfo = new List<InvoiceCryptoInfo>();
foreach (var info in this.GetCryptoData().Values) foreach (var info in this.GetCryptoData().Values)
{ {
var accounting = info.Calculate();
var cryptoInfo = new InvoiceCryptoInfo(); var cryptoInfo = new InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.CryptoCode; cryptoInfo.CryptoCode = info.CryptoCode;
cryptoInfo.Rate = info.Rate; cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString(); cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
cryptoInfo.Due = info.GetCryptoDue().ToString();
var paid = Payments.Where(p => p.Accounted && p.GetCryptoCode() == info.CryptoCode).Select(p => p.GetValue()).Sum(); cryptoInfo.Due = accounting.Due.ToString();
cryptoInfo.Paid = paid.ToString(); cryptoInfo.Paid = accounting.Paid.ToString();
cryptoInfo.TotalDue = accounting.TotalDue.ToString();
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString();
cryptoInfo.TxCount = accounting.TxCount;
cryptoInfo.CryptoPaid = accounting.CryptoPaid;
cryptoInfo.Address = info.DepositAddress; cryptoInfo.Address = info.DepositAddress;
cryptoInfo.ExRates = new Dictionary<string, double> cryptoInfo.ExRates = new Dictionary<string, double>
{ {
@@ -372,6 +378,38 @@ namespace BTCPayServer.Services.Invoices
} }
} }
public class CryptoDataAccounting
{
/// <summary>
/// Total amount of this invoice
/// </summary>
public Money TotalDue { get; set; }
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
public Money Due { get; set; }
/// <summary>
/// Total amount of the invoice paid after conversion to this crypto currency
/// </summary>
public Money Paid { get; set; }
/// <summary>
/// Total amount of the invoice paid in this currency
/// </summary>
public Money CryptoPaid { get; set; }
/// <summary>
/// Number of transactions required to pay
/// </summary>
public int TxCount { get; set; }
/// <summary>
/// Total amount of network fee to pay to the invoice
/// </summary>
public Money NetworkFee { get; set; }
}
public class CryptoData public class CryptoData
{ {
[JsonIgnore] [JsonIgnore]
@@ -386,28 +424,13 @@ namespace BTCPayServer.Services.Invoices
public Money TxFee { get; set; } public Money TxFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")] [JsonProperty(PropertyName = "depositAddress")]
public string DepositAddress { get; set; } public string DepositAddress { get; set; }
public Money GetNetworkFee() public CryptoDataAccounting Calculate()
{
var item = Calculate();
return TxFee * item.TxCount;
}
public int GetTxCount()
{
return Calculate().TxCount;
}
public Money GetTotalCryptoDue()
{
return Calculate().TotalDue;
}
private (Money TotalDue, Money Paid, int TxCount) Calculate()
{ {
var cryptoData = ParentEntity.GetCryptoData(); var cryptoData = ParentEntity.GetCryptoData();
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate) + TxFee; var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate) + TxFee;
var paid = Money.Zero; var paid = Money.Zero;
var cryptoPaid = Money.Zero;
int txCount = 1; int txCount = 1;
var payments = var payments =
ParentEntity.Payments ParentEntity.Payments
@@ -416,6 +439,8 @@ namespace BTCPayServer.Services.Invoices
.Select(_ => .Select(_ =>
{ {
paid += _.GetValue(cryptoData, CryptoCode); paid += _.GetValue(cryptoData, CryptoCode);
if (CryptoCode == _.GetCryptoCode())
cryptoPaid += _.GetValue();
return _; return _;
}) })
.TakeWhile(_ => .TakeWhile(_ =>
@@ -429,19 +454,17 @@ namespace BTCPayServer.Services.Invoices
return !paidEnough; return !paidEnough;
}) })
.ToArray(); .ToArray();
return (totalDue, paid, txCount);
}
public Money GetTotalPaid() var accounting = new CryptoDataAccounting();
{ accounting.TotalDue = totalDue;
return Calculate().Paid; accounting.Paid = paid;
} accounting.TxCount = txCount;
public Money GetCryptoDue() accounting.CryptoPaid = cryptoPaid;
{ accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
var o = Calculate(); accounting.NetworkFee = TxFee * txCount;
var v = o.TotalDue - o.Paid; return accounting;
return v < Money.Zero ? Money.Zero : v;
} }
} }
public class AccountedPaymentEntity public class AccountedPaymentEntity

View File

@@ -144,7 +144,7 @@ namespace BTCPayServer.Services.Invoices
Assigned = DateTimeOffset.UtcNow Assigned = DateTimeOffset.UtcNow
}); });
textSearch.Add(cryptoData.DepositAddress); textSearch.Add(cryptoData.DepositAddress);
textSearch.Add(cryptoData.GetTotalCryptoDue().ToString()); textSearch.Add(cryptoData.Calculate().TotalDue.ToString());
} }
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);

View File

@@ -148,6 +148,7 @@ namespace BTCPayServer.Services.Invoices
var network = _NetworkProvider.GetNetwork("BTC"); var network = _NetworkProvider.GetNetwork("BTC");
var cryptoData = invoice.GetCryptoData(network); var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData(); var cryptoDataAll = invoice.GetCryptoData();
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{ {
needSave = true; needSave = true;
@@ -160,7 +161,7 @@ namespace BTCPayServer.Services.Invoices
if (invoice.Status == "new" || invoice.Status == "expired") if (invoice.Status == "new" || invoice.Status == "expired")
{ {
var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= cryptoData.GetTotalCryptoDue()) if (totalPaid >= accounting.TotalDue)
{ {
if (invoice.Status == "new") if (invoice.Status == "new")
{ {
@@ -177,14 +178,14 @@ namespace BTCPayServer.Services.Invoices
} }
} }
if (totalPaid > cryptoData.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver") if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{ {
invoice.ExceptionStatus = "paidOver"; invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true; needSave = true;
} }
if (totalPaid < cryptoData.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{ {
Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress); Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress);
invoice.ExceptionStatus = "paidPartial"; invoice.ExceptionStatus = "paidPartial";
@@ -221,7 +222,7 @@ namespace BTCPayServer.Services.Invoices
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow) (invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&& &&
// And not enough amount confirmed // And not enough amount confirmed
(chainTotalConfirmed < cryptoData.GetTotalCryptoDue())) (chainTotalConfirmed < accounting.TotalDue))
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid"))); postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid")));
@@ -231,7 +232,7 @@ namespace BTCPayServer.Services.Invoices
else else
{ {
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= cryptoData.GetTotalCryptoDue()) if (totalConfirmed >= accounting.TotalDue)
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed"))); postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed")));
@@ -246,7 +247,7 @@ namespace BTCPayServer.Services.Invoices
var transactions = await GetPaymentsWithTransaction(invoice); var transactions = await GetPaymentsWithTransaction(invoice);
transactions = transactions.Where(t => t.Confirmations >= 6); transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= cryptoData.GetTotalCryptoDue()) if (totalConfirmed >= accounting.TotalDue)
{ {
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete"))); postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete")));
invoice.Status = "complete"; invoice.Status = "complete";