mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Filtering invoices by StartDate and EndDate
Now it's required to separate parameters with comma. Forced to do this because dates have spaces between date and time part
This commit is contained in:
parent
3b91b38014
commit
d5bd0ee781
6 changed files with 112 additions and 119 deletions
|
@ -742,6 +742,12 @@ namespace BTCPayServer.Tests
|
|||
AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver");
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true");
|
||||
AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false");
|
||||
|
||||
var time = invoice.InvoiceTime;
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}");
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToStringLowerInvariant()}");
|
||||
AssertSearchInvoice(acc, false, invoice.Id, $"startdate:{time.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss")}");
|
||||
AssertSearchInvoice(acc, false, invoice.Id, $"enddate:{time.AddSeconds(-1).ToString("yyyy-MM-dd HH:mm:ss")}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -879,22 +885,28 @@ namespace BTCPayServer.Tests
|
|||
[Trait("Fast", "Fast")]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
var filter = "storeid:abc status:abed blabhbalh ";
|
||||
var filter = "storeid:abc, status:abed, blabhbalh ";
|
||||
var search = new SearchString(filter);
|
||||
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
|
||||
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
|
||||
Assert.Equal("blabhbalh", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"]);
|
||||
Assert.Single(search.Filters["status"]);
|
||||
Assert.Equal("abc", search.Filters["storeid"].First());
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
|
||||
filter = "status:abed status:abed2";
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("status:abed status:abed2", search.ToString());
|
||||
Assert.Equal("", search.TextSearch);
|
||||
Assert.Equal("status:abed, status:abed2", search.ToString());
|
||||
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
|
||||
Assert.Equal(2, search.Filters["status"].Count);
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
|
||||
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers
|
|||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
? storeBlob.CoinSwitchSettings
|
||||
|
@ -335,7 +335,7 @@ namespace BTCPayServer.Controllers
|
|||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage?? 0,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
|
@ -410,7 +410,7 @@ namespace BTCPayServer.Controllers
|
|||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
|
@ -465,25 +465,22 @@ namespace BTCPayServer.Controllers
|
|||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
if (searchTerm == null)
|
||||
{
|
||||
searchTerm = HttpContext.Session.GetString("InvoicesSearchTerm");
|
||||
}
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
SearchTerm = searchTerm,
|
||||
Skip = skip,
|
||||
Count = count,
|
||||
StatusMessage = StatusMessage
|
||||
StatusMessage = StatusMessage,
|
||||
TimezoneOffset = timezoneOffset
|
||||
};
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
|
||||
invoiceQuery.Count = count;
|
||||
invoiceQuery.Skip = skip;
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
|
@ -504,21 +501,21 @@ namespace BTCPayServer.Controllers
|
|||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
|
||||
private InvoiceQuery GetInvoiceQuery(string searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var filterString = new SearchString(searchTerm);
|
||||
var fs = new SearchString(searchTerm);
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
TextSearch = fs.TextSearch,
|
||||
UserId = GetUserId(),
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
|
||||
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
|
||||
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
|
||||
Unusual = fs.GetFilterBool("unusual"),
|
||||
Status = fs.GetFilterArray("status"),
|
||||
ExceptionStatus = fs.GetFilterArray("exceptionstatus"),
|
||||
StoreId = fs.GetFilterArray("storeid"),
|
||||
ItemCode = fs.GetFilterArray("itemcode"),
|
||||
OrderId = fs.GetFilterArray("orderid"),
|
||||
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
|
||||
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
|
||||
};
|
||||
return invoiceQuery;
|
||||
}
|
||||
|
@ -526,13 +523,13 @@ namespace BTCPayServer.Controllers
|
|||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null)
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
|
||||
invoiceQuery.Count = int.MaxValue;
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Count = int.MaxValue;
|
||||
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
var res = model.Process(invoices, format);
|
||||
|
||||
|
@ -627,27 +624,6 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
if (invoices.SearchTerm == null)
|
||||
{
|
||||
HttpContext.Session.Remove("InvoicesSearchTerm");
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Session.SetString("InvoicesSearchTerm", invoices.SearchTerm);
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices), new
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
skip = invoices.Skip,
|
||||
count = invoices.Count,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
|
@ -696,7 +672,7 @@ namespace BTCPayServer.Controllers
|
|||
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
}
|
||||
else if(newState == "complete")
|
||||
else if (newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
|
||||
|
@ -721,18 +697,18 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
public static Dictionary<string, object> ParsePosData(string posData)
|
||||
{
|
||||
var result = new Dictionary<string,object>();
|
||||
var result = new Dictionary<string, object>();
|
||||
if (string.IsNullOrEmpty(posData))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var jObject =JObject.Parse(posData);
|
||||
var jObject = JObject.Parse(posData);
|
||||
foreach (var item in jObject)
|
||||
{
|
||||
|
||||
|
||||
switch (item.Value.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
|
@ -749,7 +725,7 @@ namespace BTCPayServer.Controllers
|
|||
result.Add(item.Key, item.Value.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -759,6 +735,6 @@ namespace BTCPayServer.Controllers
|
|||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,32 +7,14 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
{
|
||||
public class InvoicesModel
|
||||
{
|
||||
public int Skip
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Count
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Total
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SearchTerm
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Skip { get; set; }
|
||||
public int Count { get; set; }
|
||||
public int Total { get; set; }
|
||||
public string SearchTerm { get; set; }
|
||||
public int? TimezoneOffset { get; set; }
|
||||
|
||||
public List<InvoiceModel> Invoices
|
||||
{
|
||||
get; set;
|
||||
} = new List<InvoiceModel>();
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
|
||||
public class InvoiceModel
|
||||
|
@ -41,27 +23,15 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
|
||||
public string OrderId { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
public string InvoiceId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public string Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Status { get; set; }
|
||||
public bool CanMarkComplete { get; set; }
|
||||
public bool CanMarkInvalid { get; set; }
|
||||
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
|
||||
public bool ShowCheckout { get; set; }
|
||||
public string ExceptionStatus { get; set; }
|
||||
public string AmountCurrency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string AmountCurrency { get; set; }
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -15,35 +16,58 @@ namespace BTCPayServer
|
|||
str = str.Trim();
|
||||
_OriginalString = str.Trim();
|
||||
TextSearch = _OriginalString;
|
||||
var splitted = str.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var splitted = str.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Filters
|
||||
= splitted
|
||||
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Select(t => t.Split(new char[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(kv => kv.Length == 2)
|
||||
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
|
||||
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant().Trim(), kv[1]))
|
||||
.ToMultiValueDictionary(o => o.Key, o => o.Value);
|
||||
|
||||
foreach(var filter in splitted)
|
||||
{
|
||||
if(filter.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length == 2)
|
||||
{
|
||||
TextSearch = TextSearch.Replace(filter, string.Empty, StringComparison.InvariantCulture);
|
||||
}
|
||||
}
|
||||
TextSearch = TextSearch.Trim();
|
||||
var val = splitted.FirstOrDefault(a => a?.IndexOf(':', StringComparison.OrdinalIgnoreCase) == -1);
|
||||
if (val != null)
|
||||
TextSearch = val.Trim();
|
||||
else
|
||||
TextSearch = "";
|
||||
}
|
||||
|
||||
public string TextSearch
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string TextSearch { get; private set; }
|
||||
|
||||
public MultiValueDictionary<string, string> Filters { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _OriginalString;
|
||||
}
|
||||
|
||||
internal string[] GetFilterArray(string key)
|
||||
{
|
||||
return Filters.ContainsKey(key) ? Filters[key].ToArray() : null;
|
||||
}
|
||||
|
||||
internal bool? GetFilterBool(string key)
|
||||
{
|
||||
if (!Filters.ContainsKey(key))
|
||||
return null;
|
||||
|
||||
return bool.TryParse(Filters[key].First(), out var r) ?
|
||||
r : (bool?)null;
|
||||
}
|
||||
|
||||
internal DateTimeOffset? GetFilterDate(string key, int timezoneOffset)
|
||||
{
|
||||
if (!Filters.ContainsKey(key))
|
||||
return null;
|
||||
|
||||
var val = Filters[key].First();
|
||||
var success = DateTimeOffset.TryParse(val, null as IFormatProvider, DateTimeStyles.AssumeUniversal, out var r);
|
||||
if (success)
|
||||
{
|
||||
r = r.AddMinutes(timezoneOffset);
|
||||
return r;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -463,7 +463,7 @@ retry:
|
|||
{
|
||||
// Hacky way to return an empty query object. The nice way is much too elaborate:
|
||||
// https://stackoverflow.com/questions/33305495/how-to-return-empty-iqueryable-in-an-async-repository-method
|
||||
return query.Where(x => false);
|
||||
return query.Where(x => false);
|
||||
}
|
||||
query = query.Where(i => ids.Contains(i.Id));
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
|
||||
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
|
||||
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
|
||||
<li><code>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
|
||||
<li><code>enddate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created before certain date</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed status:complete</code>.
|
||||
|
@ -52,15 +54,16 @@
|
|||
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||
<a asp-action="Export" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">CSV</a>
|
||||
<a asp-action="Export" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">JSON</a>
|
||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</a>
|
||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<form asp-action="SearchInvoice" method="post" style="float:right;">
|
||||
<form asp-action="ListInvoices" method="get" style="float:right;">
|
||||
<div class="input-group">
|
||||
<input asp-for="TimezoneOffset" type="hidden" />
|
||||
<input asp-for="SearchTerm" class="form-control" style="width:300px;" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary" title="Search invoice">
|
||||
|
@ -217,5 +220,13 @@
|
|||
$(this).attr("data-switch", htmlVal);
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
var timezoneOffset = new Date().getTimezoneOffset()
|
||||
$("#TimezoneOffset").val(timezoneOffset);
|
||||
$(".export-link").each(function () {
|
||||
this.href = this.href.replace("timezoneoffset=0", "timezoneoffset=" + timezoneOffset);
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</section>
|
||||
|
|
Loading…
Add table
Reference in a new issue