mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +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.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
@ -31,6 +35,7 @@ using Xunit;
|
|||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
|
@ -679,6 +684,93 @@ namespace BTCPayServer.Tests
|
|||
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)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDeleteUsersViaApi()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
@ -12,6 +13,6 @@ public class AppSalesViewModel
|
|||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public long SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
public IEnumerable<AppSalesStatsItem> Series { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
@ -10,7 +11,7 @@ public class AppTopItemsViewModel
|
|||
public string AppType { get; set; }
|
||||
public string AppUrl { 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 bool InitialRendering { get; set; }
|
||||
}
|
||||
|
|
|
@ -226,6 +226,30 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
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()
|
||||
{
|
||||
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.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -1142,6 +1143,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
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)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
||||
|
|
|
@ -76,14 +76,14 @@ namespace BTCPayServer.Plugins.Crowdfund
|
|||
"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 items = AppService.Parse(cfS.PerksTemplate);
|
||||
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 perks = AppService.Parse(settings.PerksTemplate);
|
||||
|
@ -97,7 +97,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
|||
var total = entities.Sum(entity => entity.PaidAmount.Net);
|
||||
var itemCode = entities.Key;
|
||||
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
||||
return new ItemStats
|
||||
return new AppItemStats
|
||||
{
|
||||
ItemCode = itemCode,
|
||||
Title = perk?.Title ?? itemCode,
|
||||
|
@ -108,7 +108,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
|||
})
|
||||
.OrderByDescending(stats => stats.SalesCount);
|
||||
|
||||
return Task.FromResult<IEnumerable<ItemStats>>(perkCount);
|
||||
return Task.FromResult<IEnumerable<AppItemStats>>(perkCount);
|
||||
}
|
||||
|
||||
public override async Task<object?> GetInfo(AppData appData)
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
|
@ -75,14 +76,14 @@ namespace BTCPayServer.Plugins.PointOfSale
|
|||
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 items = AppService.Parse(posS.Template);
|
||||
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 items = AppService.Parse(settings.Template);
|
||||
|
@ -100,7 +101,7 @@ namespace BTCPayServer.Plugins.PointOfSale
|
|||
var total = entities.Sum(entity => entity.FiatPrice);
|
||||
var itemCode = entities.Key;
|
||||
var item = items.FirstOrDefault(p => p.Id == itemCode);
|
||||
return new ItemStats
|
||||
return new AppItemStats
|
||||
{
|
||||
ItemCode = itemCode,
|
||||
Title = item?.Title ?? itemCode,
|
||||
|
@ -111,7 +112,7 @@ namespace BTCPayServer.Plugins.PointOfSale
|
|||
})
|
||||
.OrderByDescending(stats => stats.SalesCount);
|
||||
|
||||
return Task.FromResult<IEnumerable<ItemStats>>(itemCount);
|
||||
return Task.FromResult<IEnumerable<AppItemStats>>(itemCount);
|
||||
}
|
||||
|
||||
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
|
@ -17,8 +15,6 @@ using BTCPayServer.Services.Invoices;
|
|||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Dapper;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
@ -87,26 +83,25 @@ namespace BTCPayServer.Services.Apps
|
|||
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)
|
||||
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
|
||||
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData,
|
||||
null, new[]
|
||||
{
|
||||
InvoiceStatus.Processing.ToString(),
|
||||
InvoiceStatus.Settled.ToString()
|
||||
});
|
||||
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData, null,
|
||||
[
|
||||
InvoiceStatus.Processing.ToString(),
|
||||
InvoiceStatus.Settled.ToString()
|
||||
]);
|
||||
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)
|
||||
{
|
||||
var series = paidInvoices
|
||||
.Aggregate(new List<InvoiceStatsItem>(), AggregateInvoiceEntitiesForStats(items))
|
||||
.Aggregate([], AggregateInvoiceEntitiesForStats(items))
|
||||
.GroupBy(entity => entity.Date)
|
||||
.Select(entities => new SalesStatsItem
|
||||
.Select(entities => new AppSalesStatsItem
|
||||
{
|
||||
Date = entities.Key,
|
||||
Label = entities.Key.ToString("MMM dd", CultureInfo.InvariantCulture),
|
||||
|
@ -119,7 +114,7 @@ namespace BTCPayServer.Services.Apps
|
|||
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
||||
if (!series.Any(e => e.Date == date))
|
||||
{
|
||||
series = series.Append(new SalesStatsItem
|
||||
series = series.Append(new AppSalesStatsItem
|
||||
{
|
||||
Date = date,
|
||||
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),
|
||||
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)
|
||||
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
|
||||
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays),
|
||||
new[]
|
||||
{
|
||||
InvoiceStatus.Processing.ToString(),
|
||||
InvoiceStatus.Settled.ToString()
|
||||
});
|
||||
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays));
|
||||
|
||||
return await salesType.GetSalesStats(app, paidInvoices, numberOfDays);
|
||||
}
|
||||
|
@ -538,26 +528,4 @@ retry:
|
|||
public int Count { 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
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
|
@ -19,10 +18,10 @@ namespace BTCPayServer.Services.Apps
|
|||
}
|
||||
public interface IHasSaleStatsAppType
|
||||
{
|
||||
Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
|
||||
Task<AppSalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
|
||||
}
|
||||
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": {
|
||||
"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