Greenfield: add text search terms to an invoice (#2648)

This commit is contained in:
Nicolas Dorier 2021-07-14 23:32:20 +09:00 committed by GitHub
parent 15be593bbd
commit 73b461f8d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 275 additions and 215 deletions

View File

@ -14,6 +14,7 @@ namespace BTCPayServer.Client
public virtual async Task<IEnumerable<InvoiceData>> 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());

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -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<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
//also test the the metadata actually got saved
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.Equal("updated",invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().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<UpdateStoreRequest>());
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<SettingsRepository>().GetSettingAsync<PoliciesSettings>())?? new PoliciesSettings();
var settings = (await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
settings.AllowLightningInternalNodeForAll = false;
await tester.PayTester.GetService<SettingsRepository>().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<GreenFieldValidationException>(async () =>
{
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
});
settings = await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>();
settings.AllowLightningInternalNodeForAll = true;
await tester.PayTester.GetService<SettingsRepository>().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<NewOnChainTransactionEvent>(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<ArgumentOutOfRangeException>(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<OutPoint>();
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
await AssertValidationError(new[] { nameof(createTxRequest.SelectedInputs) }, async () =>
{
createTxRequest.SelectedInputs = new List<OutPoint>();
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.SelectedInputs = new List<OutPoint>()
{
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<BTCPayNetwork>("BTC")) ==0.0001m );
Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork<BTCPayNetwork>("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<NewBlockEvent>(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);
}

View File

@ -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));

View File

@ -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<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> 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<InvoiceEntity> CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, CancellationToken cancellationToken = default)
internal async Task<InvoiceEntity> 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 () =>
{

View File

@ -148,9 +148,9 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null)
{
var textSearch = new List<string>();
var textSearch = new HashSet<string>();
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);

View File

@ -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",