mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-09 05:14:31 +01:00
Refactor logic for calculating due amount of invoices (#5174)
* Refactor logic for calculating due amount of invoices * Remove Money type from the accounting * Fix tests * Fix a corner case * fix bug * Rename PaymentCurrency to Currency * Fix bug * Rename PaymentCurrency -> Currency * Payment objects should have access to the InvoiceEntity * Set Currency USD in tests * Simplify some code * Remove useless code * Simplify code, kukks comment
This commit is contained in:
parent
a7def63137
commit
22435a2bf5
39 changed files with 487 additions and 424 deletions
|
@ -1,4 +1,5 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
|
@ -34,12 +35,12 @@ namespace BTCPayServer
|
|||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
|
||||
}
|
||||
|
||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||
{
|
||||
//precision 0: 10 = 0.00000010
|
||||
//precision 2: 10 = 0.00001000
|
||||
//precision 8: 10 = 10
|
||||
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
|
||||
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
|
||||
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
|
||||
builder.QueryParams.Add("assetid", AssetId.ToString());
|
||||
return builder;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
|
@ -87,13 +88,13 @@ namespace BTCPayServer
|
|||
});
|
||||
}
|
||||
|
||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||
{
|
||||
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
|
||||
builder.Host = cryptoInfoAddress;
|
||||
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
|
||||
if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
|
||||
{
|
||||
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
|
||||
builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
|
|
@ -346,165 +346,213 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
[Fact]
|
||||
public void CanCalculateDust()
|
||||
{
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Rate = 34_000m
|
||||
});
|
||||
entity.Price = 4000;
|
||||
entity.UpdateTotals();
|
||||
var accounting = entity.GetPaymentMethods().First().Calculate();
|
||||
// Exact price should be 0.117647059..., but the payment method round up to one sat
|
||||
Assert.Equal(0.11764706m, accounting.Due);
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
|
||||
Accounted = true
|
||||
});
|
||||
entity.UpdateTotals();
|
||||
Assert.Equal(0.0m, entity.NetDue);
|
||||
// The dust's value is below 1 sat
|
||||
Assert.True(entity.Dust > 0.0m);
|
||||
Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust);
|
||||
Assert.True(!entity.IsOverPaid);
|
||||
Assert.True(!entity.IsUnderPaid);
|
||||
|
||||
// Now, imagine there is litecoin. It might seem from its
|
||||
// perspecitve that there has been a slight over payment.
|
||||
// However, Calculate() should just cap it to 0.0m
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
Currency = "LTC",
|
||||
Rate = 3400m
|
||||
});
|
||||
entity.UpdateTotals();
|
||||
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
|
||||
accounting = method.Calculate();
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
#if ALTCOINS
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var entity = new InvoiceEntity();
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
entity.Networks = networkProvider;
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.Price = 5000;
|
||||
entity.UpdateTotals();
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().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(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.5m), new Key()),
|
||||
Rate = 5000,
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
||||
Assert.Equal(0.7m, accounting.Due);
|
||||
Assert.Equal(1.2m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.6m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.6m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(
|
||||
new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
|
||||
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
entity.Price = 5000;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(5.1m), accounting.Due);
|
||||
Assert.Equal(5.1m, accounting.Due);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
|
||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
||||
Assert.Equal(10.01m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
||||
Assert.Equal(4.2m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(1.0m, accounting.Paid);
|
||||
Assert.Equal(5.2m, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
|
||||
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
|
||||
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
|
||||
Assert.Equal(0.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(2.0m, accounting.Paid);
|
||||
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Currency = "LTC",
|
||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.01m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(1.5m, accounting.Paid);
|
||||
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
||||
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(3.0m, accounting.Paid);
|
||||
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Output = new TxOut(remaining, new Key()),
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(remaining), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(1.5m + remaining, accounting.Paid);
|
||||
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
|
||||
// Paying 2 BTC fee, LTC fee removed because fully paid
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */),
|
||||
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
|
||||
accounting.TotalDue);
|
||||
Assert.Equal(1, accounting.TxRequired);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
|
@ -548,27 +596,29 @@ namespace BTCPayServer.Tests
|
|||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.Price = 5000;
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
entity.UpdateTotals();
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().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);
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
Assert.Equal(1.1m, accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 10;
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
|
||||
Assert.Equal(0.99m, accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 100;
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
|
||||
Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -1884,11 +1934,6 @@ namespace BTCPayServer.Tests
|
|||
#pragma warning disable CS0618
|
||||
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var networkBTC = networkProvider.GetNetwork("BTC");
|
||||
var networkLTC = networkProvider.GetNetwork("LTC");
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
|
@ -1896,14 +1941,14 @@ namespace BTCPayServer.Tests
|
|||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.Price = 100;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
|
||||
.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
NextNetworkFee = Money.Coins(0.00000100m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m }
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
|
||||
.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
|
@ -1919,7 +1964,7 @@ namespace BTCPayServer.Tests
|
|||
new PaymentEntity()
|
||||
{
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
}
|
||||
|
@ -1928,34 +1973,33 @@ namespace BTCPayServer.Tests
|
|||
Network = networkProvider.GetNetwork("BTC"),
|
||||
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
||||
}));
|
||||
invoiceEntity.UpdateTotals();
|
||||
accounting = btc.Calculate();
|
||||
invoiceEntity.Payments.Add(
|
||||
new PaymentEntity()
|
||||
{
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
Network = networkProvider.GetNetwork("BTC")
|
||||
}
|
||||
.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
Output = new TxOut() { Value = accounting.Due }
|
||||
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
|
||||
}));
|
||||
invoiceEntity.UpdateTotals();
|
||||
accounting = btc.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Zero, accounting.DueUncapped);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
|
||||
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = ltc.Calculate();
|
||||
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
|
||||
Assert.True(accounting.DueUncapped < Money.Zero);
|
||||
|
||||
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
|
||||
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
|
||||
#pragma warning restore CS0618
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
// LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
|
||||
// and set DueUncapped to zero.
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -1761,7 +1761,7 @@ namespace BTCPayServer.Tests
|
|||
var parsedJson = await GetExport(user);
|
||||
Assert.Equal(3, parsedJson.Length);
|
||||
|
||||
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var invoiceDueAfterFirstPayment = 3 * networkFee.ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var pay1str = parsedJson[0].ToString();
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
||||
|
|
|
@ -396,7 +396,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var accounting = invoicePaymentMethod.Calculate();
|
||||
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var cryptoPaid = accounting.Paid;
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
|
||||
var rateResult = await _rateProvider.FetchRate(
|
||||
|
@ -464,7 +464,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
|
||||
var dueAmount = accounting.TotalDue;
|
||||
createPullPayment.Currency = cryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
|
@ -580,11 +580,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
CryptoCode = method.GetId().CryptoCode,
|
||||
Destination = details.GetPaymentDestination(),
|
||||
Rate = method.Rate,
|
||||
Due = accounting.DueUncapped.ToDecimal(MoneyUnit.BTC),
|
||||
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC),
|
||||
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
|
||||
Amount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC),
|
||||
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
|
||||
Due = accounting.DueUncapped,
|
||||
TotalPaid = accounting.Paid,
|
||||
PaymentMethodPaid = accounting.CryptoPaid,
|
||||
Amount = accounting.TotalDue,
|
||||
NetworkFee = accounting.NetworkFee,
|
||||
PaymentLink =
|
||||
method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due,
|
||||
Request.GetAbsoluteRoot()),
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
|
|||
return Ok(new
|
||||
{
|
||||
Txid = txid,
|
||||
AmountRemaining = (paymentMethod.Calculate().Due - amount).ToUnit(MoneyUnit.BTC),
|
||||
AmountRemaining = paymentMethod.Calculate().Due - amount.ToDecimal(MoneyUnit.BTC),
|
||||
SuccessMessage = $"Created transaction {txid}"
|
||||
});
|
||||
|
||||
|
@ -70,11 +70,11 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
var bolt11 = BOLT11PaymentRequest.Parse(destination, network);
|
||||
var paymentHash = bolt11.PaymentHash?.ToString();
|
||||
var paid = new Money(response.Details.TotalAmount.ToUnit(LightMoneyUnit.Satoshi), MoneyUnit.Satoshi);
|
||||
var paid = response.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
return Ok(new
|
||||
{
|
||||
Txid = paymentHash,
|
||||
AmountRemaining = (paymentMethod.Calculate().TotalDue - paid).ToUnit(MoneyUnit.BTC),
|
||||
AmountRemaining = paymentMethod.Calculate().TotalDue - paid,
|
||||
SuccessMessage = $"Sent payment {paymentHash}"
|
||||
});
|
||||
}
|
||||
|
|
|
@ -228,18 +228,14 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
string txId = paymentData.GetPaymentId();
|
||||
string? link = GetTransactionLink(paymentMethodId, txId);
|
||||
var paymentMethod = i.GetPaymentMethod(paymentMethodId);
|
||||
var amount = paymentData.GetValue();
|
||||
var rate = paymentMethod.Rate;
|
||||
var paid = (amount - paymentEntity.NetworkFee) * rate;
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = amount,
|
||||
Paid = paid,
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.PaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
Id = txId,
|
||||
|
@ -364,8 +360,8 @@ namespace BTCPayServer.Controllers
|
|||
if (paymentMethod != null)
|
||||
{
|
||||
accounting = paymentMethod.Calculate();
|
||||
cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
|
||||
dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
|
||||
cryptoPaid = accounting.Paid;
|
||||
dueAmount = accounting.TotalDue;
|
||||
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
|
||||
}
|
||||
|
||||
|
@ -560,7 +556,7 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var overpaidAmount = accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC);
|
||||
var overpaidAmount = accounting.OverpaidHelper;
|
||||
|
||||
if (overpaidAmount > 0)
|
||||
{
|
||||
|
@ -571,8 +567,8 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode),
|
||||
Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode),
|
||||
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
Rate = ExchangeRate(data.GetId().CryptoCode, data),
|
||||
|
@ -827,7 +823,6 @@ namespace BTCPayServer.Controllers
|
|||
var dto = invoice.EntityToDTO();
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
|
||||
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
|
||||
|
||||
switch (lang?.ToLowerInvariant())
|
||||
{
|
||||
|
@ -885,10 +880,10 @@ namespace BTCPayServer.Controllers
|
|||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ShowMoney(divisibility),
|
||||
BtcPaid = accounting.Paid.ShowMoney(divisibility),
|
||||
BtcDue = accounting.ShowMoney(accounting.Due),
|
||||
BtcPaid = accounting.ShowMoney(accounting.Paid),
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
||||
OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.NetworkFee),
|
||||
IsUnsetTopUp = invoice.IsUnsetTopUp(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
|
||||
|
|
|
@ -121,7 +121,7 @@ namespace BTCPayServer.Controllers
|
|||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
|
||||
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
|
||||
|
@ -237,7 +237,7 @@ namespace BTCPayServer.Controllers
|
|||
public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
|
||||
|
@ -314,6 +314,7 @@ namespace BTCPayServer.Controllers
|
|||
entity.RefundMail = entity.Metadata.BuyerEmail;
|
||||
}
|
||||
entity.Status = InvoiceStatusLegacy.New;
|
||||
entity.UpdateTotals();
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
|
@ -402,7 +403,7 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
using (logs.Measure("Saving invoice"))
|
||||
{
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms);
|
||||
await _InvoiceRepository.CreateInvoiceAsync(entity, additionalSearchTerms);
|
||||
foreach (var method in paymentMethods)
|
||||
{
|
||||
if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp)
|
||||
|
@ -506,7 +507,7 @@ namespace BTCPayServer.Controllers
|
|||
await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)];
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork);
|
||||
var amount = paymentMethod.Calculate().Due;
|
||||
var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid;
|
||||
|
||||
if (amount < limitValueCrypto && criteria.Above)
|
||||
|
|
|
@ -548,7 +548,7 @@ namespace BTCPayServer
|
|||
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
if (i.Type != InvoiceType.TopUp)
|
||||
{
|
||||
lnurlRequest.MinSendable = new LightMoney(pm.Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
|
||||
lnurlRequest.MinSendable = LightMoney.Coins(pm.Calculate().Due);
|
||||
if (!allowOverpay)
|
||||
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
|
||||
}
|
||||
|
|
|
@ -303,7 +303,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
bip21.Add(newUri.Uri.ToString());
|
||||
break;
|
||||
case AddressClaimDestination addressClaimDestination:
|
||||
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), new Money(blob.CryptoAmount.Value, MoneyUnit.BTC));
|
||||
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value);
|
||||
bip21New.QueryParams.Add("payout", payout.Id);
|
||||
bip21.Add(bip21New.ToString());
|
||||
break;
|
||||
|
|
|
@ -30,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
|
||||
|
@ -38,6 +39,15 @@ namespace BTCPayServer
|
|||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static DateTimeOffset TruncateMilliSeconds(this DateTimeOffset dt) => new (dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset);
|
||||
public static decimal? GetDue(this InvoiceCryptoInfo invoiceCryptoInfo)
|
||||
{
|
||||
if (invoiceCryptoInfo is null)
|
||||
return null;
|
||||
if (decimal.TryParse(invoiceCryptoInfo.Due, NumberStyles.Any, CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
return null;
|
||||
}
|
||||
public static Task<BufferizedFormFile> Bufferize(this IFormFile formFile)
|
||||
{
|
||||
return BufferizedFormFile.Bufferize(formFile);
|
||||
|
@ -382,20 +392,6 @@ namespace BTCPayServer
|
|||
return controller.View("PostRedirect", redirectVm);
|
||||
}
|
||||
|
||||
public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
|
||||
{
|
||||
var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
|
||||
var relationalCommandCache = enumerator.Private("_relationalCommandCache");
|
||||
var selectExpression = relationalCommandCache.Private<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression>("_selectExpression");
|
||||
var factory = relationalCommandCache.Private<Microsoft.EntityFrameworkCore.Query.IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");
|
||||
|
||||
var sqlGenerator = factory.Create();
|
||||
var command = sqlGenerator.GetCommand(selectExpression);
|
||||
|
||||
string sql = command.CommandText;
|
||||
return sql;
|
||||
}
|
||||
|
||||
public static BTCPayNetworkProvider ConfigureNetworkProvider(this IConfiguration configuration, Logs logs)
|
||||
{
|
||||
var _networkType = DefaultConfiguration.GetNetworkType(configuration);
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
|
@ -83,31 +84,13 @@ namespace BTCPayServer.HostedServices
|
|||
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial });
|
||||
}
|
||||
var allPaymentMethods = invoice.GetPaymentMethods();
|
||||
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
|
||||
if (allPaymentMethods.Any() && paymentMethod == null)
|
||||
return;
|
||||
if (accounting is null && invoice.Price is 0m)
|
||||
{
|
||||
accounting = new PaymentMethodAccounting()
|
||||
{
|
||||
Due = Money.Zero,
|
||||
Paid = Money.Zero,
|
||||
CryptoPaid = Money.Zero,
|
||||
DueUncapped = Money.Zero,
|
||||
NetworkFee = Money.Zero,
|
||||
TotalDue = Money.Zero,
|
||||
TxCount = 0,
|
||||
TxRequired = 0,
|
||||
MinimumTotalDue = Money.Zero,
|
||||
NetworkFeeAlreadyPaid = Money.Zero
|
||||
};
|
||||
}
|
||||
|
||||
var hasPayment = invoice.GetPayments(true).Any();
|
||||
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
|
||||
{
|
||||
var isPaid = invoice.IsUnsetTopUp() ?
|
||||
accounting.Paid > Money.Zero :
|
||||
accounting.Paid >= accounting.MinimumTotalDue;
|
||||
hasPayment :
|
||||
!invoice.IsUnderPaid;
|
||||
if (isPaid)
|
||||
{
|
||||
if (invoice.Status == InvoiceStatusLegacy.New)
|
||||
|
@ -117,13 +100,15 @@ namespace BTCPayServer.HostedServices
|
|||
if (invoice.IsUnsetTopUp())
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||
invoice.Price = (accounting.Paid - accounting.NetworkFeeAlreadyPaid).ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate;
|
||||
accounting = paymentMethod.Calculate();
|
||||
// We know there is at least one payment because hasPayment is true
|
||||
var payment = invoice.GetPayments(true).First();
|
||||
invoice.Price = payment.InvoicePaidAmount.Net;
|
||||
invoice.UpdateTotals();
|
||||
context.BlobUpdated();
|
||||
}
|
||||
else
|
||||
{
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
invoice.ExceptionStatus = invoice.IsOverPaid ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
}
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
@ -135,7 +120,7 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
if (hasPayment && invoice.IsUnderPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
|
@ -145,43 +130,43 @@ namespace BTCPayServer.HostedServices
|
|||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
||||
if (!invoice.IsUnderPaid && !invoice.IsOverPaid && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
if (invoice.IsOverPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
if (invoice.IsUnderPaid)
|
||||
{
|
||||
invoice.Status = InvoiceStatusLegacy.New;
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial;
|
||||
invoice.ExceptionStatus = hasPayment ? InvoiceExceptionStatus.PaidPartial : InvoiceExceptionStatus.None;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
var confirmedAccounting =
|
||||
paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ??
|
||||
accounting;
|
||||
var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)).ToList();
|
||||
var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum();
|
||||
var minimumDue = invoice.MinimumNetDue + unconfirmedPaid;
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
(minimumDue > 0.0m))
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatusLegacy.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
else if (minimumDue <= 0.0m)
|
||||
{
|
||||
invoice.Status = InvoiceStatusLegacy.Confirmed;
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
|
||||
|
@ -191,9 +176,11 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
if (invoice.Status == InvoiceStatusLegacy.Confirmed)
|
||||
{
|
||||
var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ??
|
||||
accounting;
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentCompleted(p)).ToList();
|
||||
var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum();
|
||||
var minimumDue = invoice.MinimumNetDue + unconfirmedPaid;
|
||||
|
||||
if (minimumDue <= 0.0m)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatusLegacy.Complete;
|
||||
|
@ -203,25 +190,6 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
}
|
||||
|
||||
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting)
|
||||
{
|
||||
PaymentMethod result = null;
|
||||
accounting = null;
|
||||
decimal nearestToZero = 0.0m;
|
||||
foreach (var paymentMethod in allPaymentMethods)
|
||||
{
|
||||
var currentAccounting = paymentMethod.Calculate();
|
||||
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
|
||||
if (result == null || distanceFromZero < nearestToZero)
|
||||
{
|
||||
result = paymentMethod;
|
||||
nearestToZero = distanceFromZero;
|
||||
accounting = currentAccounting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(invoiceId);
|
||||
|
@ -380,7 +348,7 @@ namespace BTCPayServer.HostedServices
|
|||
if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode());
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.Currency);
|
||||
var transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
|
|
|
@ -103,7 +103,7 @@ namespace BTCPayServer.HostedServices
|
|||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData:
|
||||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.Currency);
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<Attachment>
|
||||
{
|
||||
|
|
|
@ -167,7 +167,7 @@ namespace BTCPayServer.PaymentRequest
|
|||
new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
invoiceEvent.Payment.Currency,
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
|
|
@ -123,18 +123,14 @@ namespace BTCPayServer.PaymentRequest
|
|||
|
||||
string txId = paymentData.GetPaymentId();
|
||||
string link = GetTransactionLink(paymentMethodId, txId);
|
||||
var paymentMethod = entity.GetPaymentMethod(paymentMethodId);
|
||||
var amount = paymentData.GetValue();
|
||||
var rate = paymentMethod.Rate;
|
||||
var paid = (amount - paymentEntity.NetworkFee) * rate;
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = amount,
|
||||
Paid = paid,
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
Id = txId,
|
||||
|
|
|
@ -211,7 +211,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||
new Key().GetScriptPubKey(supportedPaymentMethod.AccountDerivation.ScriptPubKeyType());
|
||||
var dust = txOut.GetDustThreshold();
|
||||
var amount = paymentMethod.Calculate().Due;
|
||||
if (amount < dust)
|
||||
if (amount < dust.ToDecimal(MoneyUnit.BTC))
|
||||
throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method");
|
||||
}
|
||||
if (preparePaymentObject is null)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
@ -28,7 +29,7 @@ namespace BTCPayServer.Payments
|
|||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri)
|
||||
decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
|
@ -74,7 +75,7 @@ namespace BTCPayServer.Payments
|
|||
{
|
||||
AdditionalData = new Dictionary<string, JToken>()
|
||||
{
|
||||
{"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due,
|
||||
{"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value,
|
||||
serverUrl))}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||
decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility);
|
||||
try
|
||||
{
|
||||
due = paymentMethod.Calculate().Due.ToDecimal(MoneyUnit.BTC);
|
||||
due = paymentMethod.Calculate().Due;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
|
@ -200,7 +200,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||
if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatusLegacy.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
var pm = inv.Invoice.GetPaymentMethods().First();
|
||||
if (pm.Calculate().Due.GetValue(pm.Network as BTCPayNetwork) > 0m)
|
||||
if (pm.Calculate().Due > 0m)
|
||||
{
|
||||
await CreateNewLNInvoiceForBTCPayInvoice(inv.Invoice);
|
||||
}
|
||||
|
|
|
@ -302,7 +302,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
var paymentDetails = paymentMethod?.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (paymentMethod is null || paymentDetails is null || !paymentDetails.PayjoinEnabled)
|
||||
continue;
|
||||
due = paymentMethod.Calculate().TotalDue - output.Value;
|
||||
due = Money.Coins(paymentMethod.Calculate().TotalDue) - output.Value;
|
||||
if (due > Money.Zero)
|
||||
{
|
||||
break;
|
||||
|
|
|
@ -67,7 +67,7 @@ namespace BTCPayServer.Payments
|
|||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri)
|
||||
decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
|
@ -105,7 +105,7 @@ namespace BTCPayServer.Payments
|
|||
{
|
||||
cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
|
||||
{
|
||||
BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.Due, serverUrl),
|
||||
BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.GetDue().Value, serverUrl),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
@ -52,7 +53,7 @@ namespace BTCPayServer.Payments
|
|||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri)
|
||||
decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
|
@ -92,7 +93,7 @@ namespace BTCPayServer.Payments
|
|||
{
|
||||
invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
|
||||
{
|
||||
BOLT11 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due,
|
||||
BOLT11 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value,
|
||||
serverUrl)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ namespace BTCPayServer.Payments
|
|||
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
|
||||
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
|
||||
public abstract string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri);
|
||||
decimal cryptoInfoDue, string serverUri);
|
||||
public abstract string InvoiceViewPaymentPartialName { get; }
|
||||
|
||||
public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore);
|
||||
|
|
|
@ -89,15 +89,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
|||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.Select(entities =>
|
||||
{
|
||||
var total = entities
|
||||
.Sum(entity => entity.GetPayments(true)
|
||||
.Sum(pay =>
|
||||
{
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
}));
|
||||
var total = entities.Sum(entity => entity.PaidAmount.Net);
|
||||
var itemCode = entities.Key;
|
||||
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
||||
return new ItemStats
|
||||
|
@ -167,13 +159,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
|||
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities =>
|
||||
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
||||
{
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
})));
|
||||
entities.Sum(entity => entity.PaidAmount.Net));
|
||||
}
|
||||
|
||||
var perks = AppService.Parse( settings.PerksTemplate, false);
|
||||
|
|
|
@ -119,7 +119,7 @@ namespace BTCPayServer.Plugins.NFC
|
|||
}
|
||||
else
|
||||
{
|
||||
due = new LightMoney(lnPaymentMethod.Calculate().Due);
|
||||
due = LightMoney.Coins(lnPaymentMethod.Calculate().Due);
|
||||
}
|
||||
|
||||
if (info.MinWithdrawable > due || due > info.MaxWithdrawable)
|
||||
|
@ -135,10 +135,10 @@ namespace BTCPayServer.Plugins.NFC
|
|||
|
||||
if (lnurlPaymentMethod is not null)
|
||||
{
|
||||
Money due;
|
||||
decimal due;
|
||||
if (invoice.Type == InvoiceType.TopUp && request.Amount is not null)
|
||||
{
|
||||
due = new Money(request.Amount.Value, MoneyUnit.Satoshi);
|
||||
due = new Money(request.Amount.Value, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC);
|
||||
}
|
||||
else if (invoice.Type == InvoiceType.TopUp)
|
||||
{
|
||||
|
@ -152,7 +152,7 @@ namespace BTCPayServer.Plugins.NFC
|
|||
try
|
||||
{
|
||||
httpClient = CreateHttpClient(info.Callback);
|
||||
var amount = LightMoney.Satoshis(due.Satoshi);
|
||||
var amount = LightMoney.Coins(due);
|
||||
var actionPath = Url.Action(nameof(UILNURLController.GetLNURLForInvoice), "UILNURL",
|
||||
new { invoiceId = request.InvoiceId, cryptoCode = "BTC", amount = amount.MilliSatoshi });
|
||||
var url = Request.GetAbsoluteUri(actionPath);
|
||||
|
|
|
@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.Logging;
|
||||
|
||||
[assembly: InternalsVisibleTo("BTCPayServer.Tests")]
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
class Program
|
||||
|
|
|
@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
|||
{
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, null,
|
||||
new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due,
|
||||
new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value,
|
||||
null);
|
||||
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
|
||||
}
|
||||
|
|
|
@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
|||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
return paymentMethodDetails.Activated
|
||||
? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
|
||||
? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue}"
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
|
|
|
@ -119,16 +119,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
|||
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
var paymentData = (MoneroLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero &&
|
||||
monero.Activated &&
|
||||
monero.GetPaymentDestination() == paymentData.GetDestination() &&
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
paymentMethod.Calculate().Due > 0.0m)
|
||||
{
|
||||
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
|
||||
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.Currency];
|
||||
|
||||
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
|
||||
"create_address",
|
||||
|
|
|
@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
|||
{
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
model.InvoiceBitcoinUrl = ZcashPaymentType.Instance.GetPaymentLink(network, null,
|
||||
new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due,
|
||||
new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value,
|
||||
null);
|
||||
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
|
||||
}
|
||||
|
|
|
@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
|||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
return paymentMethodDetails.Activated
|
||||
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
|
||||
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue}"
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
|
|
|
@ -114,16 +114,16 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
|
|||
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
var paymentData = (ZcashLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var paymentMethod = invoice.GetPaymentMethod(payment.Network, ZcashPaymentType.Instance);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is ZcashLikeOnChainPaymentMethodDetails Zcash &&
|
||||
Zcash.Activated &&
|
||||
Zcash.GetPaymentDestination() == paymentData.GetDestination() &&
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
paymentMethod.Calculate().Due > 0.0m)
|
||||
{
|
||||
var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
|
||||
var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.Currency];
|
||||
|
||||
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
|
||||
"create_address",
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Apps
|
|||
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
invoiceEvent.Payment.Currency,
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
|
|
@ -183,18 +183,11 @@ namespace BTCPayServer.Services.Apps
|
|||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fiatPrice = e.GetPayments(true).Sum(pay =>
|
||||
{
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = e.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
});
|
||||
{;
|
||||
res.Add(new InvoiceStatsItem
|
||||
{
|
||||
ItemCode = e.Metadata.ItemCode,
|
||||
FiatPrice = fiatPrice,
|
||||
FiatPrice = e.PaidAmount.Net,
|
||||
Date = e.InvoiceTime.Date
|
||||
});
|
||||
}
|
||||
|
|
19
BTCPayServer/Services/Invoices/Amounts.cs
Normal file
19
BTCPayServer/Services/Invoices/Amounts.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class Amounts
|
||||
{
|
||||
public string Currency { get; set; }
|
||||
/// <summary>
|
||||
/// An amount with fee included
|
||||
/// </summary>
|
||||
public decimal Gross { get; set; }
|
||||
/// <summary>
|
||||
/// An amount without fee included
|
||||
/// </summary>
|
||||
public decimal Net { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Currency}: Net={Net}, Gross={Gross}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -64,23 +64,19 @@ namespace BTCPayServer.Services.Invoices.Export
|
|||
{
|
||||
foreach (var payment in payments)
|
||||
{
|
||||
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
|
||||
var pdata = payment.GetCryptoPaymentData();
|
||||
|
||||
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId());
|
||||
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
|
||||
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
|
||||
invoiceDue -= payment.InvoicePaidAmount.Net;
|
||||
|
||||
var target = new ExportInvoiceHolder
|
||||
{
|
||||
ReceivedDate = payment.ReceivedTime.UtcDateTime,
|
||||
PaymentId = pdata.GetPaymentId(),
|
||||
CryptoCode = cryptoCode,
|
||||
ConversionRate = pmethod.Rate,
|
||||
CryptoCode = payment.Currency,
|
||||
ConversionRate = payment.Rate,
|
||||
PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(),
|
||||
Destination = pdata.GetDestination(),
|
||||
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
|
||||
Paid = payment.PaidAmount.Gross.ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = Math.Round(payment.InvoicePaidAmount.Gross, currency.NumberDecimalDigits).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
|
||||
|
|
|
@ -378,6 +378,82 @@ namespace BTCPayServer.Services.Invoices
|
|||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, decimal> Rates
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public void UpdateTotals()
|
||||
{
|
||||
Rates = new Dictionary<string, decimal>();
|
||||
foreach (var p in GetPaymentMethods())
|
||||
{
|
||||
Rates.TryAdd(p.Currency, p.Rate);
|
||||
}
|
||||
PaidAmount = new Amounts()
|
||||
{
|
||||
Currency = Currency
|
||||
};
|
||||
foreach (var payment in GetPayments(false))
|
||||
{
|
||||
payment.Rate = Rates[payment.Currency];
|
||||
payment.InvoiceEntity = this;
|
||||
payment.UpdateAmounts();
|
||||
if (payment.Accounted)
|
||||
{
|
||||
PaidAmount.Gross += payment.InvoicePaidAmount.Gross;
|
||||
PaidAmount.Net += payment.InvoicePaidAmount.Net;
|
||||
}
|
||||
}
|
||||
NetDue = Price - PaidAmount.Net;
|
||||
MinimumNetDue = Price * (1.0m - ((decimal)PaymentTolerance / 100.0m)) - PaidAmount.Net;
|
||||
PaidFee = PaidAmount.Gross - PaidAmount.Net;
|
||||
if (NetDue < 0.0m)
|
||||
{
|
||||
// If any payment method exactly pay the invoice, the overpayment is caused by
|
||||
// rounding limitation of the underlying payment method.
|
||||
// Document this overpayment as dust, and set the net due to 0
|
||||
if (GetPaymentMethods().Any(p => p.Calculate().DueUncapped == 0.0m))
|
||||
{
|
||||
Dust = -NetDue;
|
||||
NetDue = 0.0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overpaid amount caused by payment method
|
||||
/// Example: If you need to pay 124.4 sats, the on-chain payment need to be technically rounded to 125 sats, the extra 0.6 sats shouldn't be considered an over payment.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal Dust { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The due to consider the invoice paid (can be negative if over payment)
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal NetDue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
/// <summary>
|
||||
/// Minumum due to consider the invoice paid (can be negative if overpaid)
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal MinimumNetDue { get; set; }
|
||||
public bool IsUnderPaid => MinimumNetDue > 0;
|
||||
[JsonIgnore]
|
||||
public bool IsOverPaid => NetDue < 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Total of network fee paid by accounted payments
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal PaidFee { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public InvoiceStatusLegacy Status { get; set; }
|
||||
[JsonProperty(PropertyName = "status")]
|
||||
|
@ -399,7 +475,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
}
|
||||
public List<PaymentEntity> GetPayments(string cryptoCode, bool accountedOnly)
|
||||
{
|
||||
return GetPayments(accountedOnly).Where(p => p.CryptoCode == cryptoCode).ToList();
|
||||
return GetPayments(accountedOnly).Where(p => p.Currency == cryptoCode).ToList();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(BTCPayNetworkBase network, bool accountedOnly)
|
||||
{
|
||||
|
@ -554,13 +630,13 @@ namespace BTCPayServer.Services.Invoices
|
|||
if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var minerInfo = new MinerFeeInfo();
|
||||
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
|
||||
minerInfo.TotalFee = accounting.ToSmallestUnit(accounting.NetworkFee);
|
||||
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
|
||||
.GetFee(1).Satoshi;
|
||||
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
||||
|
||||
#pragma warning disable 618
|
||||
if (info.CryptoCode == "BTC")
|
||||
if (info.Currency == "BTC")
|
||||
{
|
||||
dto.BTCPrice = cryptoInfo.Price;
|
||||
dto.Rate = cryptoInfo.Rate;
|
||||
|
@ -576,8 +652,8 @@ namespace BTCPayServer.Services.Invoices
|
|||
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
|
||||
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
|
||||
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
|
||||
dto.PaymentSubtotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(subtotalPrice));
|
||||
dto.PaymentTotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(accounting.TotalDue));
|
||||
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
|
||||
{
|
||||
Enabled = true
|
||||
|
@ -640,12 +716,12 @@ namespace BTCPayServer.Services.Invoices
|
|||
{
|
||||
continue;
|
||||
}
|
||||
r.CryptoCode = paymentMethodId.CryptoCode;
|
||||
r.Currency = paymentMethodId.CryptoCode;
|
||||
r.PaymentType = paymentMethodId.PaymentType.ToString();
|
||||
r.ParentEntity = this;
|
||||
if (Networks != null)
|
||||
{
|
||||
r.Network = Networks.GetNetwork<BTCPayNetworkBase>(r.CryptoCode);
|
||||
r.Network = Networks.GetNetwork<BTCPayNetworkBase>(r.Currency);
|
||||
if (r.Network is null)
|
||||
continue;
|
||||
}
|
||||
|
@ -671,7 +747,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
foreach (var v in paymentMethods)
|
||||
{
|
||||
var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v));
|
||||
clone.CryptoCode = null;
|
||||
clone.Currency = null;
|
||||
clone.PaymentType = null;
|
||||
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
|
||||
}
|
||||
|
@ -681,6 +757,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
cryptoData.ParentEntity = this;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
UpdateTotals();
|
||||
}
|
||||
|
||||
public InvoiceState GetInvoiceState()
|
||||
|
@ -748,6 +825,8 @@ namespace BTCPayServer.Services.Invoices
|
|||
{
|
||||
return Type == InvoiceType.TopUp && Price == 0.0m;
|
||||
}
|
||||
|
||||
public Amounts PaidAmount { get; set; }
|
||||
}
|
||||
|
||||
public enum InvoiceStatusLegacy
|
||||
|
@ -897,30 +976,31 @@ namespace BTCPayServer.Services.Invoices
|
|||
|
||||
public class PaymentMethodAccounting
|
||||
{
|
||||
public int Divisibility { get; set; }
|
||||
/// <summary>Total amount of this invoice</summary>
|
||||
public Money TotalDue { get; set; }
|
||||
public decimal TotalDue { get; set; }
|
||||
|
||||
/// <summary>Amount of crypto remaining to pay this invoice</summary>
|
||||
public Money Due { get; set; }
|
||||
public decimal Due { get; set; }
|
||||
|
||||
/// <summary>Same as Due, can be negative</summary>
|
||||
public Money DueUncapped { get; set; }
|
||||
public decimal DueUncapped { get; set; }
|
||||
|
||||
/// <summary>If DueUncapped is negative, that means user overpaid invoice</summary>
|
||||
public Money OverpaidHelper
|
||||
public decimal OverpaidHelper
|
||||
{
|
||||
get { return DueUncapped > Money.Zero ? Money.Zero : -DueUncapped; }
|
||||
get { return DueUncapped > 0.0m ? 0.0m : -DueUncapped; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of the invoice paid after conversion to this crypto currency
|
||||
/// </summary>
|
||||
public Money Paid { get; set; }
|
||||
public decimal Paid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of the invoice paid in this currency
|
||||
/// </summary>
|
||||
public Money CryptoPaid { get; set; }
|
||||
public decimal CryptoPaid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of transactions required to pay
|
||||
|
@ -934,15 +1014,25 @@ namespace BTCPayServer.Services.Invoices
|
|||
/// <summary>
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
public Money NetworkFee { get; set; }
|
||||
public decimal NetworkFee { get; set; }
|
||||
/// <summary>
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
public Money NetworkFeeAlreadyPaid { get; set; }
|
||||
public decimal NetworkFeeAlreadyPaid { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum required to be paid in order to accept invoice as paid
|
||||
/// </summary>
|
||||
public Money MinimumTotalDue { get; set; }
|
||||
public decimal MinimumTotalDue { get; set; }
|
||||
|
||||
public decimal ToSmallestUnit(decimal v)
|
||||
{
|
||||
for (int i = 0; i < Divisibility; i++)
|
||||
{
|
||||
v *= 10.0m;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
public string ShowMoney(decimal v) => MoneyExtensions.ShowMoney(v, Divisibility);
|
||||
}
|
||||
|
||||
public interface IPaymentMethod
|
||||
|
@ -959,8 +1049,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
[JsonIgnore]
|
||||
public BTCPayNetworkBase Network { get; set; }
|
||||
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[Obsolete("Use GetId().CryptoCode instead")]
|
||||
public string CryptoCode { get; set; }
|
||||
public string Currency { get; set; }
|
||||
[JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[Obsolete("Use GetId().PaymentType instead")]
|
||||
public string PaymentType { get; set; }
|
||||
|
@ -975,14 +1064,14 @@ namespace BTCPayServer.Services.Invoices
|
|||
public PaymentMethodId GetId()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType));
|
||||
return new PaymentMethodId(Currency, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public void SetId(PaymentMethodId id)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
CryptoCode = id.CryptoCode;
|
||||
Currency = id.CryptoCode;
|
||||
PaymentType = id.PaymentType.ToString();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
@ -1053,7 +1142,6 @@ namespace BTCPayServer.Services.Invoices
|
|||
DepositAddress = bitcoinPaymentMethod.DepositAddress;
|
||||
}
|
||||
PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod));
|
||||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return this;
|
||||
}
|
||||
|
@ -1068,86 +1156,58 @@ namespace BTCPayServer.Services.Invoices
|
|||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
||||
public string DepositAddress { get; set; }
|
||||
|
||||
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null)
|
||||
public PaymentMethodAccounting Calculate()
|
||||
{
|
||||
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
|
||||
var paymentMethods = ParentEntity.GetPaymentMethods();
|
||||
|
||||
var totalDue = ParentEntity.Price / Rate;
|
||||
var paid = 0m;
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
var i = ParentEntity;
|
||||
int precision = Network?.Divisibility ?? 8;
|
||||
|
||||
var totalDueNoNetworkCost = Coins(Extensions.RoundUp(totalDue, precision));
|
||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||
int txRequired = 0;
|
||||
decimal networkFeeAlreadyPaid = 0.0m;
|
||||
_ = ParentEntity.GetPayments(true)
|
||||
.Where(p => paymentPredicate(p))
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
|
||||
networkFeeAlreadyPaid += txFee;
|
||||
paid += _.GetValue(paymentMethods, GetId(), null, precision);
|
||||
if (!paidEnough)
|
||||
{
|
||||
totalDue += txFee;
|
||||
}
|
||||
|
||||
paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision);
|
||||
if (GetId() == _.GetPaymentMethodId())
|
||||
{
|
||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||
txRequired++;
|
||||
}
|
||||
|
||||
return _;
|
||||
}).ToArray();
|
||||
|
||||
var accounting = new PaymentMethodAccounting();
|
||||
accounting.TxCount = txRequired;
|
||||
if (!paidEnough)
|
||||
var thisPaymentMethodPayments = i.GetPayments(true).Where(p => GetId() == p.GetPaymentMethodId()).ToList();
|
||||
accounting.TxCount = thisPaymentMethodPayments.Count;
|
||||
accounting.TxRequired = accounting.TxCount;
|
||||
var grossDue = i.Price + i.PaidFee;
|
||||
if (i.MinimumNetDue > 0.0m)
|
||||
{
|
||||
txRequired++;
|
||||
totalDue += GetTxFee();
|
||||
accounting.TxRequired++;
|
||||
grossDue += Rate * (GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m);
|
||||
}
|
||||
accounting.Divisibility = precision;
|
||||
accounting.TotalDue = Coins(grossDue / Rate, precision);
|
||||
accounting.Paid = Coins(i.PaidAmount.Gross / Rate, precision);
|
||||
accounting.CryptoPaid = Coins(thisPaymentMethodPayments.Sum(p => p.PaidAmount.Gross), precision);
|
||||
|
||||
accounting.TotalDue = Coins(Extensions.RoundUp(totalDue, precision));
|
||||
accounting.Paid = Coins(Extensions.RoundUp(paid, precision));
|
||||
accounting.TxRequired = txRequired;
|
||||
accounting.CryptoPaid = Coins(Extensions.RoundUp(cryptoPaid, precision));
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||
accounting.NetworkFeeAlreadyPaid = Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision));
|
||||
// If the total due is 0, there is no payment tolerance to calculate
|
||||
var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0
|
||||
? 0
|
||||
: Math.Max(1.0m,
|
||||
accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)));
|
||||
accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi);
|
||||
// This one deal with the fact where it might looks like a slight over payment due to the dust of another payment method.
|
||||
// So if we detect the NetDue is zero, just cap dueUncapped to 0
|
||||
var dueUncapped = i.NetDue == 0.0m ? 0.0m : grossDue - i.PaidAmount.Gross;
|
||||
accounting.DueUncapped = Coins(dueUncapped / Rate, precision);
|
||||
accounting.Due = Max(accounting.DueUncapped, 0.0m);
|
||||
|
||||
accounting.NetworkFee = Coins((grossDue - i.Price) / Rate, precision);
|
||||
accounting.NetworkFeeAlreadyPaid = Coins(i.PaidFee / Rate, precision);
|
||||
|
||||
accounting.MinimumTotalDue = Max(Smallest(precision), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / Rate, precision));
|
||||
return accounting;
|
||||
}
|
||||
|
||||
const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m;
|
||||
private Money Coins(decimal v)
|
||||
private decimal Smallest(int precision)
|
||||
{
|
||||
if (v > MaxCoinValue)
|
||||
v = MaxCoinValue;
|
||||
// Clamp the value to not crash on degenerate invoices
|
||||
v *= 1_0000_0000m;
|
||||
if (v > long.MaxValue)
|
||||
return Money.Satoshis(long.MaxValue);
|
||||
if (v < long.MinValue)
|
||||
return Money.Satoshis(long.MinValue);
|
||||
return Money.Satoshis(v);
|
||||
decimal a = 1.0m;
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
a /= 10.0m;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private decimal GetTxFee()
|
||||
decimal Max(decimal a, decimal b) => a > b ? a : b;
|
||||
|
||||
const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m;
|
||||
internal static decimal Coins(decimal v, int precision)
|
||||
{
|
||||
return GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m;
|
||||
v = Extensions.RoundUp(v, precision);
|
||||
// Clamp the value to not crash on degenerate invoices
|
||||
if (v > MaxCoinValue)
|
||||
v = MaxCoinValue;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1209,19 +1269,60 @@ namespace BTCPayServer.Services.Invoices
|
|||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use GetpaymentMethodId().CryptoCode instead")]
|
||||
public string CryptoCode
|
||||
string _Currency;
|
||||
[JsonProperty("cryptoCode")]
|
||||
public string Currency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get
|
||||
{
|
||||
return _Currency ?? "BTC";
|
||||
}
|
||||
set
|
||||
{
|
||||
_Currency = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use GetCryptoPaymentData() instead")]
|
||||
public string CryptoPaymentData { get; set; }
|
||||
[Obsolete("Use GetpaymentMethodId().PaymentType instead")]
|
||||
public string CryptoPaymentDataType { get; set; }
|
||||
[JsonIgnore]
|
||||
public decimal Rate { get; set; }
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
public string InvoiceCurrency => InvoiceEntity.Currency;
|
||||
/// The amount paid by this payment in the <see cref="Currency"/>
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Amounts PaidAmount { get; set; }
|
||||
/// <summary>
|
||||
/// The amount paid by this payment in the <see cref="InvoiceCurrency"/>
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Amounts InvoicePaidAmount { get; set; }
|
||||
[JsonIgnore]
|
||||
public InvoiceEntity InvoiceEntity { get; set; }
|
||||
|
||||
public void UpdateAmounts()
|
||||
{
|
||||
var pd = GetCryptoPaymentData();
|
||||
if (pd is null)
|
||||
return;
|
||||
var value = pd.GetValue();
|
||||
PaidAmount = new Amounts()
|
||||
{
|
||||
Currency = Currency,
|
||||
Gross = value,
|
||||
Net = value - NetworkFee
|
||||
};
|
||||
InvoicePaidAmount = new Amounts()
|
||||
{
|
||||
Currency = InvoiceCurrency,
|
||||
Gross = PaidAmount.Gross * Rate,
|
||||
Net = PaidAmount.Net * Rate
|
||||
};
|
||||
}
|
||||
|
||||
public CryptoPaymentData GetCryptoPaymentData()
|
||||
{
|
||||
|
@ -1280,21 +1381,6 @@ namespace BTCPayServer.Services.Invoices
|
|||
#pragma warning restore CS0618
|
||||
return this;
|
||||
}
|
||||
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision)
|
||||
{
|
||||
|
||||
value = value ?? this.GetCryptoPaymentData().GetValue();
|
||||
var to = paymentMethodId;
|
||||
var from = this.GetPaymentMethodId();
|
||||
if (to == from)
|
||||
return decimal.Round(value.Value, precision);
|
||||
var fromRate = paymentMethods[from].Rate;
|
||||
var toRate = paymentMethods[to].Rate;
|
||||
|
||||
var fiatValue = fromRate * decimal.Round(value.Value, precision);
|
||||
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
|
||||
return otherCurrencyValue;
|
||||
}
|
||||
|
||||
public PaymentMethodId GetPaymentMethodId()
|
||||
{
|
||||
|
@ -1308,16 +1394,9 @@ namespace BTCPayServer.Services.Invoices
|
|||
{
|
||||
return null;
|
||||
}
|
||||
return new PaymentMethodId(CryptoCode ?? "BTC", paymentType);
|
||||
return new PaymentMethodId(Currency ?? "BTC", paymentType);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public string GetCryptoCode()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
return CryptoCode ?? "BTC";
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A record of a payment
|
||||
|
|
|
@ -12,7 +12,6 @@ using BTCPayServer.Logging;
|
|||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
@ -30,16 +29,13 @@ namespace BTCPayServer.Services.Invoices
|
|||
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
|
||||
}
|
||||
|
||||
public Logs Logs { get; }
|
||||
|
||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory,
|
||||
BTCPayNetworkProvider networks, EventAggregator eventAggregator, Logs logs)
|
||||
BTCPayNetworkProvider networks, EventAggregator eventAggregator)
|
||||
{
|
||||
Logs = logs;
|
||||
_applicationDbContextFactory = contextFactory;
|
||||
_btcPayNetworkProvider = networks;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
@ -54,14 +50,20 @@ namespace BTCPayServer.Services.Invoices
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public InvoiceEntity CreateNewInvoice()
|
||||
public InvoiceEntity CreateNewInvoice(string storeId)
|
||||
{
|
||||
return new InvoiceEntity()
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
|
||||
StoreId = storeId,
|
||||
Networks = _btcPayNetworkProvider,
|
||||
Version = InvoiceEntity.Lastest_Version,
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
Metadata = new InvoiceMetadata()
|
||||
// Truncating was an unintended side effect of previous code. Might want to remove that one day
|
||||
InvoiceTime = DateTimeOffset.UtcNow.TruncateMilliSeconds(),
|
||||
Metadata = new InvoiceMetadata(),
|
||||
#pragma warning disable CS0618
|
||||
Payments = new List<PaymentEntity>()
|
||||
#pragma warning restore CS0618
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -173,21 +175,14 @@ namespace BTCPayServer.Services.Invoices
|
|||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null)
|
||||
public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null)
|
||||
{
|
||||
var textSearch = new HashSet<string>();
|
||||
invoice = Clone(invoice);
|
||||
invoice.Networks = _btcPayNetworkProvider;
|
||||
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
#pragma warning disable CS0618
|
||||
invoice.Payments = new List<PaymentEntity>();
|
||||
#pragma warning restore CS0618
|
||||
invoice.StoreId = storeId;
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = new Data.InvoiceData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
StoreDataId = invoice.StoreId,
|
||||
Id = invoice.Id,
|
||||
Created = invoice.InvoiceTime,
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
|
@ -245,16 +240,6 @@ namespace BTCPayServer.Services.Invoices
|
|||
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private InvoiceEntity Clone(InvoiceEntity invoice)
|
||||
{
|
||||
var temp = new InvoiceData();
|
||||
temp.SetBlob(invoice);
|
||||
return temp.GetBlob(_btcPayNetworkProvider);
|
||||
}
|
||||
|
||||
public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs)
|
||||
|
@ -617,6 +602,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
entity.Metadata.BuyerEmail = entity.RefundMail;
|
||||
}
|
||||
entity.Archived = invoice.Archived;
|
||||
entity.UpdateTotals();
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
@ -828,9 +814,8 @@ namespace BTCPayServer.Services.Invoices
|
|||
{
|
||||
var paymentMethodContribution = new InvoiceStatistics.Contribution();
|
||||
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
|
||||
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate;
|
||||
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
|
||||
paymentMethodContribution.CurrencyValue = pay.InvoicePaidAmount.Net;
|
||||
paymentMethodContribution.Value = pay.PaidAmount.Net;
|
||||
return paymentMethodContribution;
|
||||
})
|
||||
.ToArray();
|
||||
|
|
|
@ -51,7 +51,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
{
|
||||
Version = 1,
|
||||
#pragma warning disable CS0618
|
||||
CryptoCode = network.CryptoCode,
|
||||
Currency = network.CryptoCode,
|
||||
#pragma warning restore CS0618
|
||||
ReceivedTime = date.UtcDateTime,
|
||||
Accounted = accounted,
|
||||
|
|
Loading…
Add table
Reference in a new issue