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.ProductInformation = new ProductInformation() { Price = 5000 };
Assert.Equal(Money.Coins(1.1m), cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), cryptoData.GetTotalCryptoDue());
var accounting = cryptoData.Calculate();
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 });
accounting = cryptoData.Calculate();
//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(1.2m), cryptoData.GetTotalCryptoDue());
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
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 });
Assert.Equal(Money.Zero, cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue());
accounting = cryptoData.Calculate();
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 });
Assert.Equal(Money.Zero, cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue());
accounting = cryptoData.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
#pragma warning restore CS0618
}

View File

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

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
@ -66,18 +67,49 @@ namespace BTCPayServer.Models
}
//"btcDue":"0.001160"
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
[JsonProperty("due")]
public string Due
{
get; set;
}
[JsonProperty("paymentUrls")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
}
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("url")]
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":{,}}

View File

@ -255,13 +255,19 @@ namespace BTCPayServer.Services.Invoices
dto.CryptoInfo = new List<InvoiceCryptoInfo>();
foreach (var info in this.GetCryptoData().Values)
{
var accounting = info.Calculate();
var cryptoInfo = new InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.CryptoCode;
cryptoInfo.Rate = info.Rate;
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.Paid = paid.ToString();
cryptoInfo.Due = accounting.Due.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.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
{
[JsonIgnore]
@ -386,28 +424,13 @@ namespace BTCPayServer.Services.Invoices
public Money TxFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")]
public string DepositAddress { get; set; }
public Money GetNetworkFee()
{
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()
public CryptoDataAccounting Calculate()
{
var cryptoData = ParentEntity.GetCryptoData();
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate) + TxFee;
var paid = Money.Zero;
var cryptoPaid = Money.Zero;
int txCount = 1;
var payments =
ParentEntity.Payments
@ -416,6 +439,8 @@ namespace BTCPayServer.Services.Invoices
.Select(_ =>
{
paid += _.GetValue(cryptoData, CryptoCode);
if (CryptoCode == _.GetCryptoCode())
cryptoPaid += _.GetValue();
return _;
})
.TakeWhile(_ =>
@ -429,19 +454,17 @@ namespace BTCPayServer.Services.Invoices
return !paidEnough;
})
.ToArray();
return (totalDue, paid, txCount);
}
public Money GetTotalPaid()
{
return Calculate().Paid;
}
public Money GetCryptoDue()
{
var o = Calculate();
var v = o.TotalDue - o.Paid;
return v < Money.Zero ? Money.Zero : v;
var accounting = new CryptoDataAccounting();
accounting.TotalDue = totalDue;
accounting.Paid = paid;
accounting.TxCount = txCount;
accounting.CryptoPaid = cryptoPaid;
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.NetworkFee = TxFee * txCount;
return accounting;
}
}
public class AccountedPaymentEntity

View File

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

View File

@ -148,6 +148,7 @@ namespace BTCPayServer.Services.Invoices
var network = _NetworkProvider.GetNetwork("BTC");
var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData();
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
needSave = true;
@ -160,7 +161,7 @@ namespace BTCPayServer.Services.Invoices
if (invoice.Status == "new" || invoice.Status == "expired")
{
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")
{
@ -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";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
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);
invoice.ExceptionStatus = "paidPartial";
@ -221,7 +222,7 @@ namespace BTCPayServer.Services.Invoices
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(chainTotalConfirmed < cryptoData.GetTotalCryptoDue()))
(chainTotalConfirmed < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid")));
@ -231,7 +232,7 @@ namespace BTCPayServer.Services.Invoices
else
{
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);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed")));
@ -246,7 +247,7 @@ namespace BTCPayServer.Services.Invoices
var transactions = await GetPaymentsWithTransaction(invoice);
transactions = transactions.Where(t => t.Confirmations >= 6);
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")));
invoice.Status = "complete";