diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index d8f9b24d1..dfac9dd86 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1598,7 +1598,7 @@ donation: var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 500, + Price = 10, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1606,6 +1606,8 @@ donation: FullNotifications = true }, Facade.Merchant); + var networkFee = Money.Satoshis(10000); + // ensure 0 invoices exported because there are no payments yet var jsonResult = user.GetController().Export("json").GetAwaiter().GetResult(); var result = Assert.IsType(jsonResult); @@ -1614,18 +1616,41 @@ donation: var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10); + // + var firstPayment = invoice.CryptoInfo[0].TotalDue - 3*networkFee; cashCow.SendToAddress(invoiceAddress, firstPayment); + Thread.Sleep(1000); // prevent race conditions, ordering payments + // look if you can reduce thread sleep, this was min value for me + + // should reduce invoice due by 0 USD because payment = network fee + cashCow.SendToAddress(invoiceAddress, networkFee); + Thread.Sleep(1000); + + // pay remaining amount + cashCow.SendToAddress(invoiceAddress, 4*networkFee); + Thread.Sleep(1000); Eventually(() => { var jsonResultPaid = user.GetController().Export("json").GetAwaiter().GetResult(); var paidresult = Assert.IsType(jsonResultPaid); Assert.Equal("application/json", paidresult.ContentType); - Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", paidresult.Content); - Assert.Contains("\"InvoicePrice\": 500.0", paidresult.Content); - Assert.Contains("\"ConversionRate\": 5000.0", paidresult.Content); - Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", paidresult.Content); + + var parsedJson = JsonConvert.DeserializeObject(paidresult.Content); + Assert.Equal(3, parsedJson.Length); + + var pay1str = parsedJson[0].ToString(); + Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); + Assert.Contains("\"InvoiceDue\": 1.5", pay1str); + Assert.Contains("\"InvoicePrice\": 10.0", pay1str); + Assert.Contains("\"ConversionRate\": 5000.0", pay1str); + Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str); + + var pay2str = parsedJson[1].ToString(); + Assert.Contains("\"InvoiceDue\": 1.5", pay2str); + + var pay3str = parsedJson[2].ToString(); + Assert.Contains("\"InvoiceDue\": 0", pay3str); }); /* diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index 543759ee3..f5b2c2bae 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -52,6 +52,8 @@ namespace BTCPayServer.Services.Invoices.Export private IEnumerable convertFromDb(InvoiceEntity invoice) { var exportList = new List(); + + var invoiceDue = invoice.ProductInformation.Price; // in this first version we are only exporting invoices that were paid foreach (var payment in invoice.GetPayments()) { @@ -64,6 +66,9 @@ namespace BTCPayServer.Services.Invoices.Export var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks); + var paidAfterNetworkFees = pdata.GetValue() - pmethod.TxFee.ToDecimal(NBitcoin.MoneyUnit.BTC); + invoiceDue -= paidAfterNetworkFees * pmethod.Rate; + var target = new ExportInvoiceHolder { ReceivedDate = payment.ReceivedTime.UtcDateTime, @@ -73,6 +78,12 @@ namespace BTCPayServer.Services.Invoices.Export PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain", Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)), Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture), + // Adding NetworkFee because Paid doesn't take into account network fees + // so if fee is 10000 satoshis, customer can essentially send infinite number of tx + // and merchant effectivelly would receive 0 BTC, invoice won't be paid + // while looking just at export you could sum Paid and assume merchant "received payments" + NetworkFee = pmethod.TxFee.ToDecimal(NBitcoin.MoneyUnit.BTC).ToString(CultureInfo.InvariantCulture), + InvoiceDue = invoiceDue, OrderId = invoice.OrderId, StoreId = invoice.StoreId, InvoiceId = invoice.Id, @@ -112,12 +123,14 @@ namespace BTCPayServer.Services.Invoices.Export public string PaymentId { get; set; } public string Destination { get; set; } public string PaymentType { get; set; } - public string Paid { get; set; } public string CryptoCode { get; set; } public decimal ConversionRate { get; set; } + public string Paid { get; set; } + public string NetworkFee { get; set; } - public decimal InvoicePrice { get; set; } public string InvoiceCurrency { get; set; } + public decimal InvoiceDue { get; set; } + public decimal InvoicePrice { get; set; } public string InvoiceItemCode { get; set; } public string InvoiceItemDesc { get; set; } public string InvoiceFullStatus { get; set; } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index d586d27b3..9c958d9e0 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -681,7 +681,7 @@ namespace BTCPayServer.Services.Invoices /// public Money NetworkFee { get; set; } /// - /// Minimum required to be paid in order to accept invocie as paid + /// Minimum required to be paid in order to accept invoice as paid /// public Money MinimumTotalDue { get; set; } }