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:
rockstardev 2019-04-25 18:13:17 -05:00 committed by Rockstar Developer
parent 3b91b38014
commit d5bd0ee781
6 changed files with 112 additions and 119 deletions

View file

@ -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]

View file

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

View file

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

View file

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

View file

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

View file

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