diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index eab823615..1d7e4eaba 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,46 +1616,42 @@ 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); - }); - /* -[ - { - "ReceivedDate": "2018-11-30T10:27:13Z", - "StoreId": "FKaSZrXLJ2tcLfCyeiYYfmZp1UM5nZ1LDecQqbwBRuHi", - "OrderId": "orderId", - "InvoiceId": "4XUkgPMaTBzwJGV9P84kPC", - "CreatedDate": "2018-11-30T10:27:06Z", - "ExpirationDate": "2018-11-30T10:42:06Z", - "MonitoringDate": "2018-11-30T11:42:06Z", - "PaymentId": "6e5755c3357b20fd66f5fc478778d81371eab341e7112ab66ed6122c0ec0d9e5-1", - "CryptoCode": "BTC", - "Destination": "mhhSEQuoM993o6vwnBeufJ4TaWov2ZUsPQ", - "PaymentType": "OnChain", - "PaymentDue": "0.10020000 BTC", - "PaymentPaid": "0.10009990 BTC", - "PaymentOverpaid": "0.00000000 BTC", - "ConversionRate": 5000.0, - "FiatPrice": 500.0, - "FiatCurrency": "USD", - "ItemCode": null, - "ItemDesc": "Some \", description", - "Status": "new" - } -] - */ + 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); + }); } } @@ -1689,8 +1687,8 @@ donation: var paidresult = Assert.IsType(exportResultPaid); Assert.Equal("application/csv", paidresult.ContentType); Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content); - Assert.Contains($",\"OnChain\",\"0.1000999\",\"BTC\",\"5000.0\",\"500.0\"", paidresult.Content); - Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content); + Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content); + Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content); }); } } diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index 543759ee3..f5afc686f 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 string Paid { get; set; } + public string NetworkFee { get; set; } public decimal ConversionRate { 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; } } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 5bbaccb6e..47d4210ff 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -41,7 +41,7 @@ namespace BTCPayServer.Services.Invoices public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath) { int retryCount = 0; - retry: +retry: try { _Engine = new DBreezeEngine(dbreezePath); @@ -385,7 +385,8 @@ namespace BTCPayServer.Services.Invoices var paymentEntity = ToObject(p.Blob, null); paymentEntity.Accounted = p.Accounted; return paymentEntity; - }).ToList(); + }) + .OrderBy(a => a.ReceivedTime).ToList(); #pragma warning restore CS0618 var state = invoice.GetInvoiceState(); entity.ExceptionStatus = state.ExceptionStatus;