diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 529851724..88fad8c2c 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -229,6 +229,33 @@ namespace BTCPayServer.Tests #pragma warning restore CS0618 } + [Fact] + public void CanAcceptInvoiceWithTolerance() + { + var entity = new InvoiceEntity(); +#pragma warning disable CS0618 + entity.Payments = new List(); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); + entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.PaymentTolerance = 0; + + + var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); + var accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(1.1m), accounting.Due); + Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); + Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); + + entity.PaymentTolerance = 10; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); + + entity.PaymentTolerance = 100; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(0), accounting.MinimumTotalDue); + + } + [Fact] public void CanPayUsingBIP70() { diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 2e98b1847..b72d23b08 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -76,16 +76,15 @@ namespace BTCPayServer.HostedServices if (paymentMethod == null) return; var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); - var isPaid = accounting.IsPaid(invoice.PaymentTolerance, out var paidOver); if (invoice.Status == "new" || invoice.Status == "expired") { - if (isPaid) + if (accounting.Paid >= accounting.MinimumTotalDue) { if (invoice.Status == "new") { context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull")); invoice.Status = "paid"; - invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null; + invoice.ExceptionStatus = accounting.Paid > accounting.MinimumTotalDue ? "paidOver" : null; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } @@ -97,7 +96,7 @@ namespace BTCPayServer.HostedServices } } - if (!isPaid && invoice.ExceptionStatus != "paidPartial" && invoice.GetPayments().Count != 0) + if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") { invoice.ExceptionStatus = "paidPartial"; context.MarkDirty(); @@ -107,19 +106,19 @@ namespace BTCPayServer.HostedServices // Just make sure RBF did not cancelled a payment if (invoice.Status == "paid") { - if (!paidOver && invoice.ExceptionStatus == "paidOver") + if (accounting.Paid == accounting.MinimumTotalDue && invoice.ExceptionStatus == "paidOver") { invoice.ExceptionStatus = null; context.MarkDirty(); } - if (paidOver&& invoice.ExceptionStatus != "paidOver") + if (accounting.Paid > accounting.MinimumTotalDue && invoice.ExceptionStatus != "paidOver") { invoice.ExceptionStatus = "paidOver"; context.MarkDirty(); } - if (!isPaid) + if (accounting.Paid < accounting.MinimumTotalDue) { invoice.Status = "new"; invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial"; @@ -130,19 +129,19 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "paid") { var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network)); - var confirmedIsPaid = confirmedAccounting.IsPaid(invoice.PaymentTolerance, out var confirmedPaidOver); + if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed - (!confirmedIsPaid)) + (confirmedAccounting.Paid < accounting.MinimumTotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } - else if (confirmedIsPaid) + else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); @@ -154,8 +153,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "confirmed") { var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network)); - var confirmedIsPaid = completedAccounting.IsPaid(invoice.PaymentTolerance, out var completedPaidOver); - if (confirmedIsPaid) + if (completedAccounting.Paid >= accounting.MinimumTotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index d10475912..80fdb1256 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -524,22 +524,10 @@ namespace BTCPayServer.Services.Invoices /// Total amount of network fee to pay to the invoice /// public Money NetworkFee { get; set; } - - public bool IsPaid(double tolerance, out bool paidOver) - { - paidOver = false; - if (Paid < TotalDue) - { - var tolerantAmount = (TotalDue.Satoshi * (tolerance == 0 ? 1 : (tolerance / 100))); - var minimumTotalDue = TotalDue.Satoshi - tolerantAmount; - return Paid.Satoshi >= minimumTotalDue; - }else if (Paid > TotalDue) - { - paidOver = true; - } - - return true; - } + /// + /// Minimum required to be paid in order to accept invocie as paid + /// + public Money MinimumTotalDue { get; set; } } public class PaymentMethod @@ -688,6 +676,10 @@ namespace BTCPayServer.Services.Invoices accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); accounting.DueUncapped = accounting.TotalDue - accounting.Paid; accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; + accounting.MinimumTotalDue = new Money(Convert.ToInt32(Math.Ceiling(accounting.TotalDue.Satoshi - + (accounting.TotalDue.Satoshi * + (ParentEntity.PaymentTolerance / 100.0) + )))); return accounting; }