From 666445e8f726d013ae8d3b82a33266c608fdeaa5 Mon Sep 17 00:00:00 2001 From: d11n Date: Thu, 12 Sep 2024 09:17:16 +0200 Subject: [PATCH] Greenfield: App endpoints for sales statistics (#6103) --- .../BTCPayServerClient.Apps.cs | 15 ++ BTCPayServer.Client/Models/AppItemStats.cs | 15 ++ BTCPayServer.Client/Models/AppSalesStats.cs | 19 ++ BTCPayServer.Tests/GreenfieldAPITests.cs | 92 +++++++++ .../Components/AppSales/AppSalesViewModel.cs | 3 +- .../AppTopItems/AppTopItemsViewModel.cs | 3 +- .../GreenField/GreenfieldAppsController.cs | 24 +++ .../GreenField/LocalBTCPayServerClient.cs | 13 ++ .../Plugins/Crowdfund/CrowdfundPlugin.cs | 8 +- .../Plugins/PointOfSale/PointOfSalePlugin.cs | 9 +- BTCPayServer/Services/Apps/AppService.cs | 58 ++---- BTCPayServer/Services/Apps/AppType.cs | 7 +- .../swagger/v1/swagger.template.apps.json | 187 ++++++++++++++++++ 13 files changed, 394 insertions(+), 59 deletions(-) create mode 100644 BTCPayServer.Client/Models/AppItemStats.cs create mode 100644 BTCPayServer.Client/Models/AppSalesStats.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.Apps.cs b/BTCPayServer.Client/BTCPayServerClient.Apps.cs index 438619c50..4c2aff7ae 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Apps.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Apps.cs @@ -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($"api/v1/apps/crowdfund/{appId}", null, HttpMethod.Get, token); } + public virtual async Task GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default) + { + if (appId == null) throw new ArgumentNullException(nameof(appId)); + var queryPayload = new Dictionary { { nameof(numberOfDays), numberOfDays } }; + return await SendHttpRequest($"api/v1/apps/{appId}/sales", queryPayload, HttpMethod.Get, token); + } + + public virtual async Task> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default) + { + if (appId == null) throw new ArgumentNullException(nameof(appId)); + var queryPayload = new Dictionary { { nameof(offset), offset }, { nameof(count), count } }; + return await SendHttpRequest>($"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)); diff --git a/BTCPayServer.Client/Models/AppItemStats.cs b/BTCPayServer.Client/Models/AppItemStats.cs new file mode 100644 index 000000000..37c809d40 --- /dev/null +++ b/BTCPayServer.Client/Models/AppItemStats.cs @@ -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; } +} diff --git a/BTCPayServer.Client/Models/AppSalesStats.cs b/BTCPayServer.Client/Models/AppSalesStats.cs new file mode 100644 index 000000000..d9cded383 --- /dev/null +++ b/BTCPayServer.Client/Models/AppSalesStats.cs @@ -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 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; } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 3bfbc0c92..ccebc0904 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -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(); + + var action = Assert.IsType(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(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() diff --git a/BTCPayServer/Components/AppSales/AppSalesViewModel.cs b/BTCPayServer/Components/AppSales/AppSalesViewModel.cs index af1029c53..d7fab9e94 100644 --- a/BTCPayServer/Components/AppSales/AppSalesViewModel.cs +++ b/BTCPayServer/Components/AppSales/AppSalesViewModel.cs @@ -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 Series { get; set; } + public IEnumerable Series { get; set; } public bool InitialRendering { get; set; } } diff --git a/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs b/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs index 3ee1839ad..1d9335d36 100644 --- a/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs +++ b/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs @@ -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 Entries { get; set; } + public List Entries { get; set; } public List SalesCount { get; set; } public bool InitialRendering { get; set; } } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index 308a7d027..fe477403e 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -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 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 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"); diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index da8429bbd..1babe8558 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -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().GetAllApps()); } + public override async Task GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default) + { + return GetFromActionResult( + await GetController().GetAppSales(appId, numberOfDays)); + } + + public override async Task> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default) + { + return GetFromActionResult>( + await GetController().GetAppTopItems(appId, offset, count)); + } + public override async Task DeleteApp(string appId, CancellationToken token = default) { HandleActionResult(await GetController().DeleteApp(appId)); diff --git a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs index f38159d09..24b3c2c37 100644 --- a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs +++ b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs @@ -76,14 +76,14 @@ namespace BTCPayServer.Plugins.Crowdfund "UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!); } - public Task GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays) + public Task GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays) { var cfS = app.GetSettings(); var items = AppService.Parse(cfS.PerksTemplate); return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays); } - public Task> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices) + public Task> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices) { var settings = appData.GetSettings(); 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>(perkCount); + return Task.FromResult>(perkCount); } public override async Task GetInfo(AppData appData) diff --git a/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs b/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs index f9eb1cdf3..deb8a6b72 100644 --- a/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs +++ b/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs @@ -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(null); } - public Task GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays) + public Task GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays) { var posS = app.GetSettings(); var items = AppService.Parse(posS.Template); return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays); } - public Task> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices) + public Task> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices) { var settings = appData.GetSettings(); 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>(itemCount); + return Task.FromResult>(itemCount); } public override Task SetDefaultSettings(AppData appData, string defaultCurrency) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index e1a5f0b55..944f1cbf8 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -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> GetItemStats(AppData appData) + public async Task> 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 GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items, + public static Task GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items, InvoiceEntity[] paidInvoices, int numberOfDays) { var series = paidInvoices - .Aggregate(new List(), 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 GetSalesStats(AppData app, int numberOfDays = 7) + public async Task 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 Series { get; set; } - } - - public class SalesStatsItem - { - public DateTime Date { get; set; } - public string Label { get; set; } - public int SalesCount { get; set; } - } } diff --git a/BTCPayServer/Services/Apps/AppType.cs b/BTCPayServer/Services/Apps/AppType.cs index a191d1d8f..c4d47aca2 100644 --- a/BTCPayServer/Services/Apps/AppType.cs +++ b/BTCPayServer/Services/Apps/AppType.cs @@ -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 GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays); + Task GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays); } public interface IHasItemStatsAppType { - Task> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities); + Task> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities); } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json index 67a9c2efc..9e96973bc 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json @@ -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" + } + } } } },