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
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, public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string orderId = null, InvoiceStatus[] status = null,
DateTimeOffset? startDate = null, DateTimeOffset? startDate = null,
DateTimeOffset? endDate = null, DateTimeOffset? endDate = null,
string textSearch = null,
bool includeArchived = false, bool includeArchived = false,
CancellationToken token = default) CancellationToken token = default)
{ {
@@ -28,7 +29,8 @@ namespace BTCPayServer.Client
if (orderId != null) if (orderId != null)
queryPayload.Add(nameof(orderId), orderId); queryPayload.Add(nameof(orderId), orderId);
if (textSearch != null)
queryPayload.Add(nameof(textSearch), textSearch);
if (status != null) if (status != null)
queryPayload.Add(nameof(status), status.Select(s=> s.ToString().ToLower()).ToArray()); 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 namespace BTCPayServer.Client.Models
{ {
public class CreateInvoiceRequest public class CreateInvoiceRequest : InvoiceDataBase
{ {
[JsonConverter(typeof(NumericStringJsonConverter))] public string[] AdditionalSearchTerms { get; set; }
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; }
}
} }
} }

View File

@@ -1,10 +1,43 @@
using System; using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models 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 Id { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }

View File

@@ -1035,10 +1035,17 @@ namespace BTCPayServer.Tests
}); });
await user.RegisterDerivationSchemeAsync("BTC"); await user.RegisterDerivationSchemeAsync("BTC");
var newInvoice = await client.CreateInvoice(user.StoreId, 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()
{
Currency = "USD",
Amount = 1,
Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"),
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{ {
RedirectAutomatically = true RedirectAutomatically = true
}}); },
AdditionalSearchTerms = new string[] { "Banana" }
});
Assert.True(newInvoice.Checkout.RedirectAutomatically); Assert.True(newInvoice.Checkout.RedirectAutomatically);
Assert.Equal(user.StoreId, newInvoice.StoreId); Assert.Equal(user.StoreId, newInvoice.StoreId);
//list //list
@@ -1048,6 +1055,15 @@ namespace BTCPayServer.Tests
Assert.Single(invoices); Assert.Single(invoices);
Assert.Equal(newInvoice.Id, invoices.First().Id); 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 //list Filtered
var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId, var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, DateTimeOffset.Now.AddHours(-1), orderId: null, status: null, DateTimeOffset.Now.AddHours(-1),

View File

@@ -54,7 +54,9 @@ namespace BTCPayServer.Controllers.GreenField
DateTimeOffset? startDate = null, DateTimeOffset? startDate = null,
[FromQuery] [FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))] [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(); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
@@ -79,7 +81,8 @@ namespace BTCPayServer.Controllers.GreenField
StartDate = startDate, StartDate = startDate,
EndDate = endDate, EndDate = endDate,
OrderId = orderId, OrderId = orderId,
Status = status Status = status,
TextSearch = textSearch
}); });
return Ok(invoices.Select(ToModel)); return Ok(invoices.Select(ToModel));

View File

@@ -153,7 +153,7 @@ namespace BTCPayServer.Controllers
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
} }
entity.PaymentTolerance = storeBlob.PaymentTolerance; 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) 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(); entity.RedirectURLTemplate = invoice.Checkout.RedirectURL?.Trim();
if (additionalTags != null) if (additionalTags != null)
entity.InternalTags.AddRange(additionalTags); 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(); InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info); logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info);
@@ -273,7 +273,7 @@ namespace BTCPayServer.Controllers
using (logs.Measure("Saving invoice")) using (logs.Measure("Saving invoice"))
{ {
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms);
} }
_ = Task.Run(async () => _ = 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 = Clone(invoice);
invoice.Networks = _Networks; invoice.Networks = _Networks;
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
@@ -210,6 +210,11 @@ namespace BTCPayServer.Services.Invoices
textSearch.Add(invoice.Metadata.OrderId); textSearch.Add(invoice.Metadata.OrderId);
textSearch.Add(invoice.StoreId); textSearch.Add(invoice.StoreId);
textSearch.Add(invoice.Metadata.BuyerEmail); textSearch.Add(invoice.Metadata.BuyerEmail);
if (additionalSearchTerms != null)
{
textSearch.AddRange(additionalSearchTerms);
}
AddToTextSearch(context, invoiceData, textSearch.ToArray()); AddToTextSearch(context, invoiceData, textSearch.ToArray());
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);

View File

@@ -35,6 +35,15 @@
"description": "Array of statuses of invoices to be fetched", "description": "Array of statuses of invoices to be fetched",
"$ref": "#/components/schemas/InvoiceStatus" "$ref": "#/components/schemas/InvoiceStatus"
}, },
{
"name": "textSearch",
"in": "query",
"required": false,
"description": "A term that can help locating specific invoices.",
"schema": {
"type": "string"
}
},
{ {
"name": "startDate", "name": "startDate",
"in": "query", "in": "query",
@@ -736,10 +745,36 @@
"PaidOver" "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": { "InvoiceData": {
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/CreateInvoiceRequest" "$ref": "#/components/schemas/InvoiceDataBase"
}, },
{ {
"type": "object", "type": "object",
@@ -929,32 +964,25 @@
] ]
}, },
"CreateInvoiceRequest": { "CreateInvoiceRequest": {
"allOf": [
{
"$ref": "#/components/schemas/InvoiceDataBase"
},
{
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"amount": { "additionalSearchTerms": {
"type": "string", "type": "array",
"format": "decimal", "items": {
"description": "The amount of the invoice" "type": "string"
}, },
"currency": { "description": "Additional search term to help you find this invoice via text search",
"type": "string", "nullable": true
"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"
} }
} }
}
]
}, },
"UpdateInvoiceRequest": { "UpdateInvoiceRequest": {
"type": "object", "type": "object",