mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 06:21:44 +01:00
Greenfield: App endpoints for sales statistics (#6103)
This commit is contained in:
parent
36bada8feb
commit
666445e8f7
13 changed files with 394 additions and 59 deletions
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -58,6 +59,20 @@ public partial class BTCPayServerClient
|
||||||
return await SendHttpRequest<CrowdfundAppData>($"api/v1/apps/crowdfund/{appId}", null, HttpMethod.Get, token);
|
return await SendHttpRequest<CrowdfundAppData>($"api/v1/apps/crowdfund/{appId}", null, HttpMethod.Get, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task<AppSalesStats> GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||||
|
var queryPayload = new Dictionary<string, object> { { nameof(numberOfDays), numberOfDays } };
|
||||||
|
return await SendHttpRequest<AppSalesStats>($"api/v1/apps/{appId}/sales", queryPayload, HttpMethod.Get, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<List<AppItemStats>> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||||
|
var queryPayload = new Dictionary<string, object> { { nameof(offset), offset }, { nameof(count), count } };
|
||||||
|
return await SendHttpRequest<List<AppItemStats>>($"api/v1/apps/{appId}/top-items", queryPayload, HttpMethod.Get, token);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task DeleteApp(string appId, CancellationToken token = default)
|
public virtual async Task DeleteApp(string appId, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||||
|
|
15
BTCPayServer.Client/Models/AppItemStats.cs
Normal file
15
BTCPayServer.Client/Models/AppItemStats.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class AppItemStats
|
||||||
|
{
|
||||||
|
public string ItemCode { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public int SalesCount { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal Total { get; set; }
|
||||||
|
public string TotalFormatted { get; set; }
|
||||||
|
}
|
19
BTCPayServer.Client/Models/AppSalesStats.cs
Normal file
19
BTCPayServer.Client/Models/AppSalesStats.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class AppSalesStats
|
||||||
|
{
|
||||||
|
public int SalesCount { get; set; }
|
||||||
|
public IEnumerable<AppSalesStatsItem> Series { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppSalesStatsItem
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
|
public int SalesCount { get; set; }
|
||||||
|
}
|
|
@ -16,7 +16,11 @@ using BTCPayServer.Models.InvoicingModels;
|
||||||
using BTCPayServer.NTag424;
|
using BTCPayServer.NTag424;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.PayoutProcessors;
|
using BTCPayServer.PayoutProcessors;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Notifications.Blobs;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
@ -31,6 +35,7 @@ using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||||
|
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
|
@ -679,6 +684,93 @@ namespace BTCPayServer.Tests
|
||||||
Assert.False(apps[2].Archived);
|
Assert.False(apps[2].Archived);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanGetAppStats()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
await user.GrantAccessAsync(true);
|
||||||
|
await user.MakeAdmin();
|
||||||
|
await user.RegisterDerivationSchemeAsync("BTC");
|
||||||
|
var client = await user.CreateClient();
|
||||||
|
|
||||||
|
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||||
|
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||||
|
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||||
|
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
|
||||||
|
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
|
||||||
|
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
|
||||||
|
|
||||||
|
// empty states
|
||||||
|
var posSales = await client.GetAppSales(posApp.Id);
|
||||||
|
Assert.NotNull(posSales);
|
||||||
|
Assert.Equal(0, posSales.SalesCount);
|
||||||
|
Assert.Equal(7, posSales.Series.Count());
|
||||||
|
|
||||||
|
var crowdfundSales = await client.GetAppSales(crowdfundApp.Id);
|
||||||
|
Assert.NotNull(crowdfundSales);
|
||||||
|
Assert.Equal(0, crowdfundSales.SalesCount);
|
||||||
|
Assert.Equal(7, crowdfundSales.Series.Count());
|
||||||
|
|
||||||
|
var posTopItems = await client.GetAppTopItems(posApp.Id);
|
||||||
|
Assert.Empty(posTopItems);
|
||||||
|
|
||||||
|
var crowdfundItems = await client.GetAppTopItems(crowdfundApp.Id);
|
||||||
|
Assert.Empty(crowdfundItems);
|
||||||
|
|
||||||
|
// with sales - fiddle invoices via the UI controller
|
||||||
|
var uiPosController = tester.PayTester.GetController<UIPointOfSaleController>();
|
||||||
|
|
||||||
|
var action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Static, 1, choiceKey: item1.Id).GetAwaiter().GetResult());
|
||||||
|
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||||
|
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i1Id));
|
||||||
|
|
||||||
|
var cart = new JObject {
|
||||||
|
["cart"] = new JArray
|
||||||
|
{
|
||||||
|
new JObject { ["id"] = item2.Id, ["count"] = 4 },
|
||||||
|
new JObject { ["id"] = item3.Id, ["count"] = 2 }
|
||||||
|
},
|
||||||
|
["subTotal"] = 14,
|
||||||
|
["total"] = 14,
|
||||||
|
["amounts"] = new JArray()
|
||||||
|
}.ToString();
|
||||||
|
action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Cart, 7, posData: cart).GetAwaiter().GetResult());
|
||||||
|
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||||
|
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i2Id));
|
||||||
|
|
||||||
|
await user.PayInvoice(i1Id!.ToString());
|
||||||
|
await user.PayInvoice(i2Id!.ToString());
|
||||||
|
|
||||||
|
posSales = await client.GetAppSales(posApp.Id);
|
||||||
|
Assert.Equal(7, posSales.SalesCount);
|
||||||
|
Assert.Equal(7, posSales.Series.Count());
|
||||||
|
Assert.Equal(0, posSales.Series.First().SalesCount);
|
||||||
|
Assert.Equal(7, posSales.Series.Last().SalesCount);
|
||||||
|
|
||||||
|
posTopItems = await client.GetAppTopItems(posApp.Id);
|
||||||
|
Assert.Equal(3, posTopItems.Count);
|
||||||
|
Assert.Equal(item2.Id, posTopItems[0].ItemCode);
|
||||||
|
Assert.Equal(4, posTopItems[0].SalesCount);
|
||||||
|
|
||||||
|
Assert.Equal(item3.Id, posTopItems[1].ItemCode);
|
||||||
|
Assert.Equal(2, posTopItems[1].SalesCount);
|
||||||
|
|
||||||
|
Assert.Equal(item1.Id, posTopItems[2].ItemCode);
|
||||||
|
Assert.Equal(1, posTopItems[2].SalesCount);
|
||||||
|
|
||||||
|
// with count and offset
|
||||||
|
posTopItems = await client.GetAppTopItems(posApp.Id,1, 5);
|
||||||
|
Assert.Equal(2, posTopItems.Count);
|
||||||
|
Assert.Equal(item3.Id, posTopItems[0].ItemCode);
|
||||||
|
Assert.Equal(2, posTopItems[0].SalesCount);
|
||||||
|
|
||||||
|
Assert.Equal(item1.Id, posTopItems[1].ItemCode);
|
||||||
|
Assert.Equal(1, posTopItems[1].SalesCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanDeleteUsersViaApi()
|
public async Task CanDeleteUsersViaApi()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
|
||||||
namespace BTCPayServer.Components.AppSales;
|
namespace BTCPayServer.Components.AppSales;
|
||||||
|
@ -12,6 +13,6 @@ public class AppSalesViewModel
|
||||||
public string AppUrl { get; set; }
|
public string AppUrl { get; set; }
|
||||||
public string DataUrl { get; set; }
|
public string DataUrl { get; set; }
|
||||||
public long SalesCount { get; set; }
|
public long SalesCount { get; set; }
|
||||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
public IEnumerable<AppSalesStatsItem> Series { get; set; }
|
||||||
public bool InitialRendering { get; set; }
|
public bool InitialRendering { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
|
||||||
namespace BTCPayServer.Components.AppTopItems;
|
namespace BTCPayServer.Components.AppTopItems;
|
||||||
|
@ -10,7 +11,7 @@ public class AppTopItemsViewModel
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public string AppUrl { get; set; }
|
public string AppUrl { get; set; }
|
||||||
public string DataUrl { get; set; }
|
public string DataUrl { get; set; }
|
||||||
public List<ItemStats> Entries { get; set; }
|
public List<AppItemStats> Entries { get; set; }
|
||||||
public List<int> SalesCount { get; set; }
|
public List<int> SalesCount { get; set; }
|
||||||
public bool InitialRendering { get; set; }
|
public bool InitialRendering { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,6 +226,30 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("~/api/v1/apps/{appId}/sales")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
public async Task<IActionResult> GetAppSales(string appId, [FromQuery] int numberOfDays = 7)
|
||||||
|
{
|
||||||
|
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||||
|
if (app == null) return AppNotFound();
|
||||||
|
|
||||||
|
var stats = await _appService.GetSalesStats(app, numberOfDays);
|
||||||
|
return Ok(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("~/api/v1/apps/{appId}/top-items")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
public async Task<IActionResult> GetAppTopItems(string appId, [FromQuery] int offset = 0, [FromQuery] int count = 10)
|
||||||
|
{
|
||||||
|
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||||
|
if (app == null) return AppNotFound();
|
||||||
|
|
||||||
|
var stats = (await _appService.GetItemStats(app)).ToList();
|
||||||
|
var max = Math.Min(count, stats.Count - offset);
|
||||||
|
var items = stats.GetRange(offset, max);
|
||||||
|
return Ok(items);
|
||||||
|
}
|
||||||
|
|
||||||
private IActionResult AppNotFound()
|
private IActionResult AppNotFound()
|
||||||
{
|
{
|
||||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||||
|
|
|
@ -14,6 +14,7 @@ using BTCPayServer.Controllers.GreenField;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Security.Greenfield;
|
using BTCPayServer.Security.Greenfield;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -1142,6 +1143,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
await GetController<GreenfieldAppsController>().GetAllApps());
|
await GetController<GreenfieldAppsController>().GetAllApps());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<AppSalesStats> GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<AppSalesStats>(
|
||||||
|
await GetController<GreenfieldAppsController>().GetAppSales(appId, numberOfDays));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<List<AppItemStats>> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<List<AppItemStats>>(
|
||||||
|
await GetController<GreenfieldAppsController>().GetAppTopItems(appId, offset, count));
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task DeleteApp(string appId, CancellationToken token = default)
|
public override async Task DeleteApp(string appId, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
||||||
|
|
|
@ -76,14 +76,14 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||||
"UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!);
|
"UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
public Task<AppSalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||||
{
|
{
|
||||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||||
var items = AppService.Parse(cfS.PerksTemplate);
|
var items = AppService.Parse(cfS.PerksTemplate);
|
||||||
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
public Task<IEnumerable<AppItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
||||||
{
|
{
|
||||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||||
var perks = AppService.Parse(settings.PerksTemplate);
|
var perks = AppService.Parse(settings.PerksTemplate);
|
||||||
|
@ -97,7 +97,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||||
var total = entities.Sum(entity => entity.PaidAmount.Net);
|
var total = entities.Sum(entity => entity.PaidAmount.Net);
|
||||||
var itemCode = entities.Key;
|
var itemCode = entities.Key;
|
||||||
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
||||||
return new ItemStats
|
return new AppItemStats
|
||||||
{
|
{
|
||||||
ItemCode = itemCode,
|
ItemCode = itemCode,
|
||||||
Title = perk?.Title ?? itemCode,
|
Title = perk?.Title ?? itemCode,
|
||||||
|
@ -108,7 +108,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||||
})
|
})
|
||||||
.OrderByDescending(stats => stats.SalesCount);
|
.OrderByDescending(stats => stats.SalesCount);
|
||||||
|
|
||||||
return Task.FromResult<IEnumerable<ItemStats>>(perkCount);
|
return Task.FromResult<IEnumerable<AppItemStats>>(perkCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<object?> GetInfo(AppData appData)
|
public override async Task<object?> GetInfo(AppData appData)
|
||||||
|
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Abstractions.Services;
|
using BTCPayServer.Abstractions.Services;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
|
@ -75,14 +76,14 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||||
return Task.FromResult<object?>(null);
|
return Task.FromResult<object?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
public Task<AppSalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||||
{
|
{
|
||||||
var posS = app.GetSettings<PointOfSaleSettings>();
|
var posS = app.GetSettings<PointOfSaleSettings>();
|
||||||
var items = AppService.Parse(posS.Template);
|
var items = AppService.Parse(posS.Template);
|
||||||
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
public Task<IEnumerable<AppItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
||||||
{
|
{
|
||||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||||
var items = AppService.Parse(settings.Template);
|
var items = AppService.Parse(settings.Template);
|
||||||
|
@ -100,7 +101,7 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||||
var total = entities.Sum(entity => entity.FiatPrice);
|
var total = entities.Sum(entity => entity.FiatPrice);
|
||||||
var itemCode = entities.Key;
|
var itemCode = entities.Key;
|
||||||
var item = items.FirstOrDefault(p => p.Id == itemCode);
|
var item = items.FirstOrDefault(p => p.Id == itemCode);
|
||||||
return new ItemStats
|
return new AppItemStats
|
||||||
{
|
{
|
||||||
ItemCode = itemCode,
|
ItemCode = itemCode,
|
||||||
Title = item?.Title ?? itemCode,
|
Title = item?.Title ?? itemCode,
|
||||||
|
@ -111,7 +112,7 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||||
})
|
})
|
||||||
.OrderByDescending(stats => stats.SalesCount);
|
.OrderByDescending(stats => stats.SalesCount);
|
||||||
|
|
||||||
return Task.FromResult<IEnumerable<ItemStats>>(itemCount);
|
return Task.FromResult<IEnumerable<AppItemStats>>(itemCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Configuration;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
|
@ -17,8 +15,6 @@ using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Ganss.Xss;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
|
@ -87,26 +83,25 @@ namespace BTCPayServer.Services.Apps
|
||||||
return await appType.GetInfo(appData);
|
return await appType.GetInfo(appData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData appData)
|
public async Task<IEnumerable<AppItemStats>> GetItemStats(AppData appData)
|
||||||
{
|
{
|
||||||
if (GetAppType(appData.AppType) is not IHasItemStatsAppType salesType)
|
if (GetAppType(appData.AppType) is not IHasItemStatsAppType salesType)
|
||||||
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
|
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
|
||||||
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData,
|
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData, null,
|
||||||
null, new[]
|
[
|
||||||
{
|
InvoiceStatus.Processing.ToString(),
|
||||||
InvoiceStatus.Processing.ToString(),
|
InvoiceStatus.Settled.ToString()
|
||||||
InvoiceStatus.Settled.ToString()
|
]);
|
||||||
});
|
|
||||||
return await salesType.GetItemStats(appData, paidInvoices);
|
return await salesType.GetItemStats(appData, paidInvoices);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<SalesStats> GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items,
|
public static Task<AppSalesStats> GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items,
|
||||||
InvoiceEntity[] paidInvoices, int numberOfDays)
|
InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||||
{
|
{
|
||||||
var series = paidInvoices
|
var series = paidInvoices
|
||||||
.Aggregate(new List<InvoiceStatsItem>(), AggregateInvoiceEntitiesForStats(items))
|
.Aggregate([], AggregateInvoiceEntitiesForStats(items))
|
||||||
.GroupBy(entity => entity.Date)
|
.GroupBy(entity => entity.Date)
|
||||||
.Select(entities => new SalesStatsItem
|
.Select(entities => new AppSalesStatsItem
|
||||||
{
|
{
|
||||||
Date = entities.Key,
|
Date = entities.Key,
|
||||||
Label = entities.Key.ToString("MMM dd", CultureInfo.InvariantCulture),
|
Label = entities.Key.ToString("MMM dd", CultureInfo.InvariantCulture),
|
||||||
|
@ -119,7 +114,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
||||||
if (!series.Any(e => e.Date == date))
|
if (!series.Any(e => e.Date == date))
|
||||||
{
|
{
|
||||||
series = series.Append(new SalesStatsItem
|
series = series.Append(new AppSalesStatsItem
|
||||||
{
|
{
|
||||||
Date = date,
|
Date = date,
|
||||||
Label = date.ToString("MMM dd", CultureInfo.InvariantCulture)
|
Label = date.ToString("MMM dd", CultureInfo.InvariantCulture)
|
||||||
|
@ -127,23 +122,18 @@ namespace BTCPayServer.Services.Apps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(new SalesStats
|
return Task.FromResult(new AppSalesStats
|
||||||
{
|
{
|
||||||
SalesCount = series.Sum(i => i.SalesCount),
|
SalesCount = series.Sum(i => i.SalesCount),
|
||||||
Series = series.OrderBy(i => i.Label)
|
Series = series.OrderBy(i => i.Label)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SalesStats> GetSalesStats(AppData app, int numberOfDays = 7)
|
public async Task<AppSalesStats> GetSalesStats(AppData app, int numberOfDays = 7)
|
||||||
{
|
{
|
||||||
if (GetAppType(app.AppType) is not IHasSaleStatsAppType salesType)
|
if (GetAppType(app.AppType) is not IHasSaleStatsAppType salesType)
|
||||||
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
|
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
|
||||||
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays),
|
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays));
|
||||||
new[]
|
|
||||||
{
|
|
||||||
InvoiceStatus.Processing.ToString(),
|
|
||||||
InvoiceStatus.Settled.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return await salesType.GetSalesStats(app, paidInvoices, numberOfDays);
|
return await salesType.GetSalesStats(app, paidInvoices, numberOfDays);
|
||||||
}
|
}
|
||||||
|
@ -538,26 +528,4 @@ retry:
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ItemStats
|
|
||||||
{
|
|
||||||
public string ItemCode { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public int SalesCount { get; set; }
|
|
||||||
public decimal Total { get; set; }
|
|
||||||
public string TotalFormatted { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SalesStats
|
|
||||||
{
|
|
||||||
public int SalesCount { get; set; }
|
|
||||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SalesStatsItem
|
|
||||||
{
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public string Label { get; set; }
|
|
||||||
public int SalesCount { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Apps
|
namespace BTCPayServer.Services.Apps
|
||||||
{
|
{
|
||||||
|
@ -19,10 +18,10 @@ namespace BTCPayServer.Services.Apps
|
||||||
}
|
}
|
||||||
public interface IHasSaleStatsAppType
|
public interface IHasSaleStatsAppType
|
||||||
{
|
{
|
||||||
Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
|
Task<AppSalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
|
||||||
}
|
}
|
||||||
public interface IHasItemStatsAppType
|
public interface IHasItemStatsAppType
|
||||||
{
|
{
|
||||||
Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities);
|
Task<IEnumerable<AppItemStats>> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -345,6 +345,130 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/apps/{appId}/sales": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Apps"
|
||||||
|
],
|
||||||
|
"operationId": "Apps_GetAppSales",
|
||||||
|
"summary": "Get app sales statistics",
|
||||||
|
"description": "Returns sales statistics for the app",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "appId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The app ID",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "numberOfDays",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "How many of the last days",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"default": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "App sales statistics",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AppSalesStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "App with specified ID was not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmodifystoresettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/apps/{appId}/top-items": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Apps"
|
||||||
|
],
|
||||||
|
"operationId": "Apps_GetAppTopItems",
|
||||||
|
"summary": "Get app top items statistics",
|
||||||
|
"description": "Returns top items statistics for the app",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "appId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The app ID",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "count",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "How many of the items",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"default": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Offset for paging",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "App top items statistics",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AppItemStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "App with specified ID was not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmodifystoresettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/stores/{storeId}/apps": {
|
"/api/v1/stores/{storeId}/apps": {
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
@ -887,6 +1011,69 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"AppSalesStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"salesCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 615,
|
||||||
|
"description": "Total sales in that period"
|
||||||
|
},
|
||||||
|
"series": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AppSalesStatsItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AppSalesStatsItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "UNIX timestamp of the beginning of that day"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Short date as label",
|
||||||
|
"example": "Jul 21"
|
||||||
|
},
|
||||||
|
"salesCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 21,
|
||||||
|
"description": "Total sales on that day"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AppItemStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"itemCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item ID"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item Name"
|
||||||
|
},
|
||||||
|
"salesCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 21,
|
||||||
|
"description": "Total sales of that item"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal",
|
||||||
|
"description": "The total amount of sales of that item"
|
||||||
|
},
|
||||||
|
"totalFormatted": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The formatted total amount of sales of that item",
|
||||||
|
"example": "615.21 USD"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue