diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index e54d6998b..74d740d2f 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -14,6 +14,7 @@ namespace BTCPayServer.Client public virtual async Task> GetInvoices(string storeId, string orderId = null, InvoiceStatus[] status = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, + string textSearch = null, bool includeArchived = false, CancellationToken token = default) { @@ -28,7 +29,8 @@ namespace BTCPayServer.Client if (orderId != null) queryPayload.Add(nameof(orderId), orderId); - + if (textSearch != null) + queryPayload.Add(nameof(textSearch), textSearch); if (status != null) queryPayload.Add(nameof(status), status.Select(s=> s.ToString().ToLower()).ToArray()); diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 739b7c857..9738655aa 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -7,35 +7,8 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Client.Models { - public class CreateInvoiceRequest + public class CreateInvoiceRequest : InvoiceDataBase { - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal Amount { get; set; } - public string Currency { get; set; } - public JObject Metadata { get; set; } - public CheckoutOptions Checkout { get; set; } = new CheckoutOptions(); - - public class CheckoutOptions - { - - [JsonConverter(typeof(StringEnumConverter))] - public SpeedPolicy? SpeedPolicy { get; set; } - - public string[] PaymentMethods { get; set; } - - [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] - [JsonProperty("expirationMinutes")] - public TimeSpan? Expiration { get; set; } - [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] - [JsonProperty("monitoringMinutes")] - public TimeSpan? Monitoring { get; set; } - - public double? PaymentTolerance { get; set; } - [JsonProperty("redirectURL")] - public string RedirectURL { get; set; } - - public bool? RedirectAutomatically { get; set; } - public string DefaultLanguage { get; set; } - } + public string[] AdditionalSearchTerms { get; set; } } } diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index a457c3f22..a1e21763e 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -1,10 +1,43 @@ using System; +using BTCPayServer.Client.JsonConverters; +using BTCPayServer.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Client.Models { - public class InvoiceData : CreateInvoiceRequest + public class InvoiceDataBase + { + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Amount { get; set; } + public string Currency { get; set; } + public JObject Metadata { get; set; } + public CheckoutOptions Checkout { get; set; } = new CheckoutOptions(); + public class CheckoutOptions + { + + [JsonConverter(typeof(StringEnumConverter))] + public SpeedPolicy? SpeedPolicy { get; set; } + + public string[] PaymentMethods { get; set; } + + [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] + [JsonProperty("expirationMinutes")] + public TimeSpan? Expiration { get; set; } + [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] + [JsonProperty("monitoringMinutes")] + public TimeSpan? Monitoring { get; set; } + + public double? PaymentTolerance { get; set; } + [JsonProperty("redirectURL")] + public string RedirectURL { get; set; } + + public bool? RedirectAutomatically { get; set; } + public string DefaultLanguage { get; set; } + } + } + public class InvoiceData : InvoiceDataBase { public string Id { get; set; } public string StoreId { get; set; } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 0f57c1244..eb62acd82 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -171,7 +171,7 @@ namespace BTCPayServer.Tests // Should 404 if user doesn't exist await AssertHttpError(404, async () => await adminClient.DeleteUser("lol user id")); - + user = tester.NewAccount(); await user.GrantAccessAsync(); var badClient = await user.CreateClient(Policies.CanCreateInvoice); @@ -1035,10 +1035,17 @@ namespace BTCPayServer.Tests }); await user.RegisterDerivationSchemeAsync("BTC"); var newInvoice = await client.CreateInvoice(user.StoreId, - new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"), Checkout = new CreateInvoiceRequest.CheckoutOptions() + new CreateInvoiceRequest() { - RedirectAutomatically = true - }}); + Currency = "USD", + Amount = 1, + Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"), + Checkout = new CreateInvoiceRequest.CheckoutOptions() + { + RedirectAutomatically = true + }, + AdditionalSearchTerms = new string[] { "Banana" } + }); Assert.True(newInvoice.Checkout.RedirectAutomatically); Assert.Equal(user.StoreId, newInvoice.StoreId); //list @@ -1048,21 +1055,30 @@ namespace BTCPayServer.Tests Assert.Single(invoices); Assert.Equal(newInvoice.Id, invoices.First().Id); + invoices = await viewOnly.GetInvoices(user.StoreId, textSearch: "Banana"); + Assert.NotNull(invoices); + Assert.Single(invoices); + Assert.Equal(newInvoice.Id, invoices.First().Id); + + invoices = await viewOnly.GetInvoices(user.StoreId, textSearch: "apples"); + Assert.NotNull(invoices); + Assert.Empty(invoices); + //list Filtered - var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId, - orderId: null, status: null, DateTimeOffset.Now.AddHours(-1), - DateTimeOffset.Now.AddHours(1)); + var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId, + orderId: null, status: null, DateTimeOffset.Now.AddHours(-1), + DateTimeOffset.Now.AddHours(1)); - Assert.NotNull(invoicesFiltered); - Assert.Single(invoicesFiltered); - Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id); + Assert.NotNull(invoicesFiltered); + Assert.Single(invoicesFiltered); + Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id); - //list Yesterday - var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId, - orderId: null, status: null, DateTimeOffset.Now.AddDays(-2), - DateTimeOffset.Now.AddDays(-1)); - Assert.NotNull(invoicesYesterday); - Assert.Empty(invoicesYesterday); + //list Yesterday + var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId, + orderId: null, status: null, DateTimeOffset.Now.AddDays(-2), + DateTimeOffset.Now.AddDays(-1)); + Assert.NotNull(invoicesYesterday); + Assert.Empty(invoicesYesterday); // Error, startDate and endDate inverted await AssertValidationError(new[] { "startDate", "endDate" }, @@ -1080,30 +1096,30 @@ namespace BTCPayServer.Tests //list Existing OrderId var invoicesExistingOrderId = await viewOnly.GetInvoices(user.StoreId, orderId: newInvoice.Metadata["orderId"].ToString()); - Assert.NotNull(invoicesExistingOrderId); - Assert.Single(invoicesFiltered); - Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id); + Assert.NotNull(invoicesExistingOrderId); + Assert.Single(invoicesFiltered); + Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id); - //list NonExisting OrderId - var invoicesNonExistingOrderId = - await viewOnly.GetInvoices(user.StoreId, orderId: "NonExistingOrderId"); - Assert.NotNull(invoicesNonExistingOrderId); - Assert.Empty(invoicesNonExistingOrderId); + //list NonExisting OrderId + var invoicesNonExistingOrderId = + await viewOnly.GetInvoices(user.StoreId, orderId: "NonExistingOrderId"); + Assert.NotNull(invoicesNonExistingOrderId); + Assert.Empty(invoicesNonExistingOrderId); + + //list Existing Status + var invoicesExistingStatus = + await viewOnly.GetInvoices(user.StoreId, status: new[] { newInvoice.Status }); + Assert.NotNull(invoicesExistingStatus); + Assert.Single(invoicesExistingStatus); + Assert.Equal(newInvoice.Id, invoicesExistingStatus.First().Id); + + //list NonExisting Status + var invoicesNonExistingStatus = await viewOnly.GetInvoices(user.StoreId, + status: new[] { BTCPayServer.Client.Models.InvoiceStatus.Invalid }); + Assert.NotNull(invoicesNonExistingStatus); + Assert.Empty(invoicesNonExistingStatus); - //list Existing Status - var invoicesExistingStatus = - await viewOnly.GetInvoices(user.StoreId, status:new []{newInvoice.Status}); - Assert.NotNull(invoicesExistingStatus); - Assert.Single(invoicesExistingStatus); - Assert.Equal(newInvoice.Id, invoicesExistingStatus.First().Id); - //list NonExisting Status - var invoicesNonExistingStatus = await viewOnly.GetInvoices(user.StoreId, - status: new []{BTCPayServer.Client.Models.InvoiceStatus.Invalid}); - Assert.NotNull(invoicesNonExistingStatus); - Assert.Empty(invoicesNonExistingStatus); - - //get var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); Assert.Equal(newInvoice.Metadata, invoice.Metadata); @@ -1127,7 +1143,7 @@ namespace BTCPayServer.Tests { Status = InvoiceStatus.Invalid }); - + await AssertHttpError(403, async () => { await viewOnly.UpdateInvoice(user.StoreId, invoice.Id, @@ -1142,14 +1158,14 @@ namespace BTCPayServer.Tests Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}") }); - Assert.Equal("updated",invoice.Metadata["itemCode"].Value()); - Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values().Sum()); + Assert.Equal("updated", invoice.Metadata["itemCode"].Value()); + Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values().Sum()); //also test the the metadata actually got saved invoice = await client.GetInvoice(user.StoreId, invoice.Id); - Assert.Equal("updated",invoice.Metadata["itemCode"].Value()); - Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values().Sum()); - + Assert.Equal("updated", invoice.Metadata["itemCode"].Value()); + Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values().Sum()); + //archive await AssertHttpError(403, async () => { @@ -1238,11 +1254,11 @@ namespace BTCPayServer.Tests var store = await client.GetStore(user.StoreId); Assert.False(store.LazyPaymentMethods); store.LazyPaymentMethods = true; - store = await client.UpdateStore(store.Id, + store = await client.UpdateStore(store.Id, JObject.FromObject(store).ToObject()); Assert.True(store.LazyPaymentMethods); - invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() {Amount = 1, Currency = "USD"}); + invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 1, Currency = "USD" }); paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); Assert.Single(paymentMethods); Assert.False(paymentMethods.First().Activated); @@ -1342,7 +1358,7 @@ namespace BTCPayServer.Tests Assert.NotEqual(0, info.BlockHeight); } } - + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task NotificationAPITests() @@ -1381,7 +1397,7 @@ namespace BTCPayServer.Tests Assert.Empty(await viewOnlyClient.GetNotifications(true)); Assert.Empty(await viewOnlyClient.GetNotifications(false)); } - + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task OnChainPaymentMethodAPITests() @@ -1393,7 +1409,7 @@ namespace BTCPayServer.Tests var client = await user.CreateClient(Policies.CanModifyStoreSettings); var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings); - var store = await client.CreateStore(new CreateStoreRequest() {Name = "test store"}); + var store = await client.CreateStore(new CreateStoreRequest() { Name = "test store" }); Assert.Empty(await client.GetStoreOnChainPaymentMethods(store.Id)); await AssertHttpError(403, async () => @@ -1408,37 +1424,37 @@ namespace BTCPayServer.Tests { await client.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC"); }); - + Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC", - new OnChainPaymentMethodData() {Enabled = true, DerivationScheme = xpub})).Addresses.First().Address); - + new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address); + var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", - new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub}); - - Assert.Equal(xpub,method.DerivationScheme); + new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub }); + + Assert.Equal(xpub, method.DerivationScheme); method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", - new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") }); + new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") }); method = await client.GetStoreOnChainPaymentMethod(store.Id, "BTC"); Assert.Equal("lol", method.Label); Assert.Equal(RootedKeyPath.Parse("01020304/1/2/3"), method.AccountKeyPath); - Assert.Equal(xpub,method.DerivationScheme); - - + Assert.Equal(xpub, method.DerivationScheme); + + Assert.Equal(firstAddress, (await viewOnlyClient.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC")).Addresses.First().Address); await AssertHttpError(403, async () => { await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); }); - await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); - await AssertHttpError(404, async () => - { - await client.GetStoreOnChainPaymentMethod(store.Id, "BTC"); - }); + await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); + await AssertHttpError(404, async () => + { + await client.GetStoreOnChainPaymentMethod(store.Id, "BTC"); + }); } - + [Fact(Timeout = 60 * 2 * 1000)] [Trait("Lightning", "Lightning")] [Trait("Integration", "Integration")] @@ -1467,13 +1483,13 @@ namespace BTCPayServer.Tests await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC"); }); await admin.RegisterLightningNodeAsync("BTC", false); - + var method = await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC"); await AssertHttpError(403, async () => { await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); }); - await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); + await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); await AssertHttpError(404, async () => { await adminClient.GetStoreOnChainPaymentMethod(store.Id, "BTC"); @@ -1530,22 +1546,22 @@ namespace BTCPayServer.Tests }); }); - var settings = (await tester.PayTester.GetService().GetSettingAsync())?? new PoliciesSettings(); + var settings = (await tester.PayTester.GetService().GetSettingAsync()) ?? new PoliciesSettings(); settings.AllowLightningInternalNodeForAll = false; await tester.PayTester.GetService().UpdateSetting(settings); var nonAdminUser = tester.NewAccount(); await nonAdminUser.GrantAccessAsync(false); - var nonAdminUserClient= await nonAdminUser.CreateClient(Policies.CanModifyStoreSettings); - + var nonAdminUserClient = await nonAdminUser.CreateClient(Policies.CanModifyStoreSettings); + await AssertHttpError(404, async () => { - await nonAdminUserClient.GetStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC"); + await nonAdminUserClient.GetStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC"); }); await Assert.ThrowsAsync(async () => { await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method); }); - + settings = await tester.PayTester.GetService().GetSettingAsync(); settings.AllowLightningInternalNodeForAll = true; await tester.PayTester.GetService().UpdateSetting(settings); @@ -1559,33 +1575,33 @@ namespace BTCPayServer.Tests { using var tester = ServerTester.Create(); await tester.StartAsync(); - + var user = tester.NewAccount(); await user.GrantAccessAsync(true); - + var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings); var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings); var walletId = await user.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true); - + //view only clients can't do jack shit with this API await AssertHttpError(403, async () => { - await viewOnlyClient.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); + await viewOnlyClient.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode); }); - var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); + var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode); Assert.Equal(0m, overview.Balance); - - - var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode ); - Assert.NotNull( fee.FeeRate); + + + var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode); + Assert.NotNull(fee.FeeRate); await AssertHttpError(403, async () => { - await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); + await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode); }); - var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); - var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); - var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true ); + var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode); + var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode); + var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true); Assert.Equal(address.Address, address2.Address); Assert.NotEqual(address.Address, address3.Address); await AssertHttpError(403, async () => @@ -1596,25 +1612,25 @@ namespace BTCPayServer.Tests uint256 txhash = null; await tester.WaitForEvent(async () => { - txhash = await tester.ExplorerNode.SendToAddressAsync( + txhash = await tester.ExplorerNode.SendToAddressAsync( BitcoinAddress.Create(address3.Address, tester.ExplorerClient.Network.NBitcoinNetwork), new Money(0.01m, MoneyUnit.BTC)); }); await tester.ExplorerNode.GenerateAsync(1); - - var address4 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, false ); + + var address4 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, false); Assert.NotEqual(address3.Address, address4.Address); await client.UnReserveOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode); - var address5 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true ); + var address5 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true); Assert.Equal(address5.Address, address4.Address); - + var utxo = Assert.Single(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode)); - Assert.Equal(0.01m, utxo.Amount); - Assert.Equal(txhash, utxo.Outpoint.Hash); - overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); - Assert.Equal(0.01m, overview.Balance); - + Assert.Equal(0.01m, utxo.Amount); + Assert.Equal(txhash, utxo.Outpoint.Hash); + overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode); + Assert.Equal(0.01m, overview.Balance); + //the simplest request: var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync(); var createTxRequest = new CreateOnChainTransactionRequest() @@ -1631,7 +1647,7 @@ namespace BTCPayServer.Tests }; await AssertHttpError(403, async () => { - await viewOnlyClient.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest ); + await viewOnlyClient.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest); }); await Assert.ThrowsAsync(async () => { @@ -1645,11 +1661,11 @@ namespace BTCPayServer.Tests createTxRequest); }); Transaction tx; - - tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, - createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - - + + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + + Assert.NotNull(tx); Assert.Contains(tx.Outputs, txout => txout.IsTo(nodeAddress) && txout.Value.ToDecimal(MoneyUnit.BTC) == 0.001m); Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed); @@ -1659,17 +1675,17 @@ namespace BTCPayServer.Tests tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); Assert.NotNull(tx); - Assert.True(Assert.Single(tx.Outputs).IsTo(nodeAddress) ); + Assert.True(Assert.Single(tx.Outputs).IsTo(nodeAddress)); Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed); createTxRequest.NoChange = false; //coin selection - await AssertValidationError(new []{nameof(createTxRequest.SelectedInputs)}, async () => - { - createTxRequest.SelectedInputs = new List(); - tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, - createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - }); + await AssertValidationError(new[] { nameof(createTxRequest.SelectedInputs) }, async () => + { + createTxRequest.SelectedInputs = new List(); + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + }); createTxRequest.SelectedInputs = new List() { utxo.Outpoint @@ -1677,55 +1693,55 @@ namespace BTCPayServer.Tests tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); createTxRequest.SelectedInputs = null; - + //destination testing - await AssertValidationError(new []{ "Destinations"}, async () => - { - createTxRequest.Destinations[0].Amount = utxo.Amount; - tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, - createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - }); - + await AssertValidationError(new[] { "Destinations" }, async () => + { + createTxRequest.Destinations[0].Amount = utxo.Amount; + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + }); + createTxRequest.Destinations[0].SubtractFromAmount = true; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - - - await AssertValidationError(new []{ "Destinations[0]"}, async () => - { - createTxRequest.Destinations[0].Amount = 0m; - tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, - createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - }); - + + + await AssertValidationError(new[] { "Destinations[0]" }, async () => + { + createTxRequest.Destinations[0].Amount = 0m; + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + }); + //dest can be a bip21 - + //cant use bip with subtractfromamount createTxRequest.Destinations[0].Amount = null; createTxRequest.Destinations[0].Destination = $"bitcoin:{nodeAddress}?amount=0.001"; - await AssertValidationError(new []{ "Destinations[0]"}, async () => - { - tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, - createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - }); + await AssertValidationError(new[] { "Destinations[0]" }, async () => + { + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + }); //if amt specified, it overrides bip21 amount createTxRequest.Destinations[0].Amount = 0.0001m; createTxRequest.Destinations[0].SubtractFromAmount = false; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork("BTC")) ==0.0001m ); - + Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork("BTC")) == 0.0001m); + //fee rate test createTxRequest.FeeRate = FeeRate.Zero; - await AssertValidationError(new []{ "FeeRate"}, async () => - { - tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, - createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); - }); + await AssertValidationError(new[] { "FeeRate" }, async () => + { + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + }); - createTxRequest.FeeRate = new FeeRate(5.0m); - + createTxRequest.FeeRate = new FeeRate(5.0m); + createTxRequest.Destinations[0].Amount = 0.001m; createTxRequest.Destinations[0].Destination = nodeAddress.ToString(); createTxRequest.Destinations[0].SubtractFromAmount = false; @@ -1735,30 +1751,30 @@ namespace BTCPayServer.Tests createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); createTxRequest.ProceedWithBroadcast = true; - var txdata= + var txdata = await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest); Assert.Equal(TransactionStatus.Unconfirmed, txdata.Status); Assert.Null(txdata.BlockHeight); Assert.Null(txdata.BlockHash); Assert.NotNull(await tester.ExplorerClient.GetTransactionAsync(txdata.TransactionHash)); - + await AssertHttpError(403, async () => { await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString()); }); await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString()); - + await AssertHttpError(403, async () => { await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode); }); Assert.True(Assert.Single( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, - new[] {TransactionStatus.Confirmed})).TransactionHash == utxo.Outpoint.Hash); + new[] { TransactionStatus.Confirmed })).TransactionHash == utxo.Outpoint.Hash); Assert.Contains( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, - new[] {TransactionStatus.Unconfirmed}), data => data.TransactionHash == txdata.TransactionHash); + new[] { TransactionStatus.Unconfirmed }), data => data.TransactionHash == txdata.TransactionHash); Assert.Contains( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode), data => data.TransactionHash == txdata.TransactionHash); await tester.WaitForEvent(async () => @@ -1769,7 +1785,7 @@ namespace BTCPayServer.Tests Assert.Contains( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, - new[] {TransactionStatus.Confirmed}), data => data.TransactionHash == txdata.TransactionHash); + new[] { TransactionStatus.Confirmed }), data => data.TransactionHash == txdata.TransactionHash); } diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 044ebdfc1..6b95f9197 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -54,7 +54,9 @@ namespace BTCPayServer.Controllers.GreenField DateTimeOffset? startDate = null, [FromQuery] [ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))] - DateTimeOffset? endDate = null, [FromQuery] bool includeArchived = false) + DateTimeOffset? endDate = null, + string textSearch = null, + [FromQuery] bool includeArchived = false) { var store = HttpContext.GetStoreData(); if (store == null) @@ -79,7 +81,8 @@ namespace BTCPayServer.Controllers.GreenField StartDate = startDate, EndDate = endDate, OrderId = orderId, - Status = status + Status = status, + TextSearch = textSearch }); return Ok(invoices.Select(ToModel)); diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index c63041d5f..01b261e2f 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -153,7 +153,7 @@ namespace BTCPayServer.Controllers excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); } entity.PaymentTolerance = storeBlob.PaymentTolerance; - return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken); + return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken); } internal async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) @@ -183,10 +183,10 @@ namespace BTCPayServer.Controllers entity.RedirectURLTemplate = invoice.Checkout.RedirectURL?.Trim(); if (additionalTags != null) entity.InternalTags.AddRange(additionalTags); - return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken); + return await CreateInvoiceCoreRaw(entity, store, excludeFilter, invoice.AdditionalSearchTerms, cancellationToken); } - internal async Task CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, CancellationToken cancellationToken = default) + internal async Task CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, string[] additionalSearchTerms = null, CancellationToken cancellationToken = default) { InvoiceLogs logs = new InvoiceLogs(); logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info); @@ -273,7 +273,7 @@ namespace BTCPayServer.Controllers using (logs.Measure("Saving invoice")) { - entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); + entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms); } _ = Task.Run(async () => { diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 0b246649b..e1214c64b 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -148,9 +148,9 @@ namespace BTCPayServer.Services.Invoices } } - public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice) + public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null) { - var textSearch = new List(); + var textSearch = new HashSet(); invoice = Clone(invoice); invoice.Networks = _Networks; invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); @@ -210,6 +210,11 @@ namespace BTCPayServer.Services.Invoices textSearch.Add(invoice.Metadata.OrderId); textSearch.Add(invoice.StoreId); textSearch.Add(invoice.Metadata.BuyerEmail); + + if (additionalSearchTerms != null) + { + textSearch.AddRange(additionalSearchTerms); + } AddToTextSearch(context, invoiceData, textSearch.ToArray()); await context.SaveChangesAsync().ConfigureAwait(false); diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index ce22d4980..613953edc 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -35,6 +35,15 @@ "description": "Array of statuses of invoices to be fetched", "$ref": "#/components/schemas/InvoiceStatus" }, + { + "name": "textSearch", + "in": "query", + "required": false, + "description": "A term that can help locating specific invoices.", + "schema": { + "type": "string" + } + }, { "name": "startDate", "in": "query", @@ -736,10 +745,36 @@ "PaidOver" ] }, + "InvoiceDataBase": { + "properties": { + "amount": { + "type": "string", + "format": "decimal", + "description": "The amount of the invoice" + }, + "currency": { + "type": "string", + "nullable": true, + "description": "The currency the invoice will use" + }, + "metadata": { + "$ref": "#/components/schemas/InvoiceMetadata" + }, + "checkout": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/CheckoutOptions" + } + ], + "description": "Additional settings to customize the checkout flow" + } + } + }, "InvoiceData": { "allOf": [ { - "$ref": "#/components/schemas/CreateInvoiceRequest" + "$ref": "#/components/schemas/InvoiceDataBase" }, { "type": "object", @@ -759,15 +794,15 @@ }, "createdTime": { "description": "The creation time of the invoice", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] + "allOf": [ { "$ref": "#/components/schemas/UnixTimestamp" } ] }, "expirationTime": { "description": "The expiration time of the invoice", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] + "allOf": [ { "$ref": "#/components/schemas/UnixTimestamp" } ] }, "monitoringTime": { "description": "The monitoring time of the invoice", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] + "allOf": [ { "$ref": "#/components/schemas/UnixTimestamp" } ] }, "status": { "$ref": "#/components/schemas/InvoiceStatus", @@ -789,7 +824,7 @@ "type": "object", "description": "Any json object in any schema you want. Will be rendered on a best effort basis in terms of style on the invoice details UI" }, - "InvoiceMetadata": { + "InvoiceMetadata": { "type": "object", "additionalProperties": true, "description": "Additional information around the invoice that can be supplied. The mentioned properties are all optional and you can introduce any json format you wish.", @@ -929,32 +964,25 @@ ] }, "CreateInvoiceRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "amount": { - "type": "string", - "format": "decimal", - "description": "The amount of the invoice" + "allOf": [ + { + "$ref": "#/components/schemas/InvoiceDataBase" }, - "currency": { - "type": "string", - "nullable": true, - "description": "The currency the invoice will use" - }, - "metadata": { - "$ref": "#/components/schemas/InvoiceMetadata" - }, - "checkout": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/CheckoutOptions" + { + "type": "object", + "additionalProperties": false, + "properties": { + "additionalSearchTerms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional search term to help you find this invoice via text search", + "nullable": true } - ], - "description": "Additional settings to customize the checkout flow" + } } - } + ] }, "UpdateInvoiceRequest": { "type": "object", @@ -989,13 +1017,13 @@ "expirationMinutes": { "nullable": true, "description": "The number of minutes after which an invoice becomes expired. Defaults to the store's settings. (The default store settings is 15)", - "allOf": [ {"$ref": "#/components/schemas/TimeSpanMinutes"}] + "allOf": [ { "$ref": "#/components/schemas/TimeSpanMinutes" } ] }, "monitoringMinutes": { "type": "integer", "nullable": true, "description": "The number of minutes after an invoice expired after which we are still monitoring for incoming payments. Defaults to the store's settings. (The default store settings is 1440, 1 day)", - "allOf": [ {"$ref": "#/components/schemas/TimeSpanMinutes"}] + "allOf": [ { "$ref": "#/components/schemas/TimeSpanMinutes" } ] }, "paymentTolerance": { "type": "number", @@ -1109,7 +1137,7 @@ }, "receivedDate": { "description": "The date the payment was recorded", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] + "allOf": [ { "$ref": "#/components/schemas/UnixTimestamp" } ] }, "value": { "type": "string",