mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-12 10:30:47 +01:00
Plugins can now build apps (#4608)
* Plugins can now build apps * fix tests * fixup * pluginize existing apps * Test fixes part 1 * Test fixes part 2 * Fix Crowdfund namespace * Syntax * More namespace fixes * Markup * Test fix * upstream fixes * Add plugin icon * Fix nullable build warnings * allow pre popualting app creation * Fixes after merge * Make link methods async * Use AppData as parameter for ConfigureLink * GetApps by AppType * Use ConfigureLink on dashboard * Rename method * Add properties to indicate stats support * Property updates * Test fixes * Clean up imports * Fixes after merge --------- Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
parent
a671632fde
commit
f74ea14d8b
42 changed files with 899 additions and 652 deletions
|
@ -11,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Models.WalletViewModels;
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
@ -622,10 +623,11 @@ namespace BTCPayServer.Tests
|
||||||
var apps = user.GetController<UIAppsController>();
|
var apps = user.GetController<UIAppsController>();
|
||||||
var pos = user.GetController<UIPointOfSaleController>();
|
var pos = user.GetController<UIPointOfSaleController>();
|
||||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||||
var appType = AppType.PointOfSale.ToString();
|
var appType = PointOfSaleApp.AppType;
|
||||||
vm.AppName = "test";
|
vm.AppName = "test";
|
||||||
vm.SelectedAppType = appType;
|
vm.SelectedAppType = appType;
|
||||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||||
|
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
var app = appList.Apps[0];
|
var app = appList.Apps[0];
|
||||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||||
|
|
|
@ -4,11 +4,11 @@ using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Tests.Logging;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
|
@ -34,18 +34,16 @@ namespace BTCPayServer.Tests
|
||||||
await user.GrantAccessAsync();
|
await user.GrantAccessAsync();
|
||||||
var user2 = tester.NewAccount();
|
var user2 = tester.NewAccount();
|
||||||
await user2.GrantAccessAsync();
|
await user2.GrantAccessAsync();
|
||||||
var stores = user.GetController<UIStoresController>();
|
|
||||||
var apps = user.GetController<UIAppsController>();
|
var apps = user.GetController<UIAppsController>();
|
||||||
var apps2 = user2.GetController<UIAppsController>();
|
var apps2 = user2.GetController<UIAppsController>();
|
||||||
var crowdfund = user.GetController<UICrowdfundController>();
|
var crowdfund = user.GetController<UICrowdfundController>();
|
||||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
var appType = CrowdfundApp.AppType;
|
||||||
var appType = AppType.Crowdfund.ToString();
|
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
|
||||||
Assert.NotNull(vm.SelectedAppType);
|
Assert.Equal(appType, vm.SelectedAppType);
|
||||||
Assert.Null(vm.AppName);
|
Assert.Null(vm.AppName);
|
||||||
vm.AppName = "test";
|
vm.AppName = "test";
|
||||||
vm.SelectedAppType = appType;
|
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||||
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
|
|
||||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
var app = appList.Apps[0];
|
var app = appList.Apps[0];
|
||||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||||
|
@ -61,8 +59,8 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||||
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||||
Assert.Empty(appList.Apps);
|
Assert.Empty(appList.Apps);
|
||||||
}
|
}
|
||||||
|
@ -79,10 +77,11 @@ namespace BTCPayServer.Tests
|
||||||
var apps = user.GetController<UIAppsController>();
|
var apps = user.GetController<UIAppsController>();
|
||||||
var crowdfund = user.GetController<UICrowdfundController>();
|
var crowdfund = user.GetController<UICrowdfundController>();
|
||||||
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
|
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
|
||||||
var appType = AppType.Crowdfund.ToString();
|
var appType = CrowdfundApp.AppType;
|
||||||
vm.AppName = "test";
|
vm.AppName = "test";
|
||||||
vm.SelectedAppType = appType;
|
vm.SelectedAppType = appType;
|
||||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||||
|
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
var app = appList.Apps[0];
|
var app = appList.Apps[0];
|
||||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||||
|
@ -105,7 +104,7 @@ namespace BTCPayServer.Tests
|
||||||
Amount = new decimal(0.01)
|
Amount = new decimal(0.01)
|
||||||
}, default));
|
}, default));
|
||||||
|
|
||||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
|
||||||
|
|
||||||
//Scenario 2: Not Enabled But Admin - Allowed
|
//Scenario 2: Not Enabled But Admin - Allowed
|
||||||
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||||
|
@ -113,8 +112,8 @@ namespace BTCPayServer.Tests
|
||||||
RedirectToCheckout = false,
|
RedirectToCheckout = false,
|
||||||
Amount = new decimal(0.01)
|
Amount = new decimal(0.01)
|
||||||
}, default));
|
}, default));
|
||||||
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
|
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id));
|
||||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
|
||||||
|
|
||||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||||
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
||||||
|
@ -170,10 +169,10 @@ namespace BTCPayServer.Tests
|
||||||
var apps = user.GetController<UIAppsController>();
|
var apps = user.GetController<UIAppsController>();
|
||||||
var crowdfund = user.GetController<UICrowdfundController>();
|
var crowdfund = user.GetController<UICrowdfundController>();
|
||||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||||
var appType = AppType.Crowdfund.ToString();
|
var appType = CrowdfundApp.AppType;
|
||||||
vm.AppName = "test";
|
vm.AppName = "test";
|
||||||
vm.SelectedAppType = appType;
|
vm.SelectedAppType = appType;
|
||||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
var app = appList.Apps[0];
|
var app = appList.Apps[0];
|
||||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||||
|
@ -193,7 +192,7 @@ namespace BTCPayServer.Tests
|
||||||
var publicApps = user.GetController<UICrowdfundController>();
|
var publicApps = user.GetController<UICrowdfundController>();
|
||||||
|
|
||||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||||
|
|
||||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
|
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
|
||||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
|
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
|
||||||
|
@ -217,7 +216,7 @@ namespace BTCPayServer.Tests
|
||||||
}, Facade.Merchant);
|
}, Facade.Merchant);
|
||||||
|
|
||||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
|
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||||
|
|
||||||
Assert.Equal(0m, model.Info.CurrentAmount);
|
Assert.Equal(0m, model.Info.CurrentAmount);
|
||||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||||
|
@ -226,12 +225,12 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
TestLogs.LogInformation("Let's check current amount change once payment is confirmed");
|
TestLogs.LogInformation("Let's check current amount change once payment is confirmed");
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||||
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
|
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, invoice.BtcDue);
|
||||||
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
|
await tester.ExplorerNode.GenerateAsync(1); // By default invoice confirmed at 1 block
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
{
|
{
|
||||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||||
Assert.Equal(1m, model.Info.CurrentAmount);
|
Assert.Equal(1m, model.Info.CurrentAmount);
|
||||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||||
});
|
});
|
||||||
|
@ -279,7 +278,7 @@ namespace BTCPayServer.Tests
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
{
|
{
|
||||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
|
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||||
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,9 @@ using System.Threading.Tasks;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services.Apps;
|
|
||||||
using BTCPayServer.Tests.Logging;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
@ -32,10 +31,11 @@ namespace BTCPayServer.Tests
|
||||||
var apps = user.GetController<UIAppsController>();
|
var apps = user.GetController<UIAppsController>();
|
||||||
var pos = user.GetController<UIPointOfSaleController>();
|
var pos = user.GetController<UIPointOfSaleController>();
|
||||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||||
var appType = AppType.PointOfSale.ToString();
|
var appType = PointOfSaleApp.AppType;
|
||||||
vm.AppName = "test";
|
vm.AppName = "test";
|
||||||
vm.SelectedAppType = appType;
|
vm.SelectedAppType = appType;
|
||||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||||
|
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
var app = appList.Apps[0];
|
var app = appList.Apps[0];
|
||||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||||
|
|
|
@ -35,6 +35,8 @@ using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Bitcoin;
|
using BTCPayServer.Payments.Bitcoin;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Payments.PayJoin.Sender;
|
using BTCPayServer.Payments.PayJoin.Sender;
|
||||||
|
using BTCPayServer.Plugins.PayButton;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
using BTCPayServer.Security.Bitpay;
|
using BTCPayServer.Security.Bitpay;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
@ -1953,14 +1955,13 @@ namespace BTCPayServer.Tests
|
||||||
var apps = user.GetController<UIAppsController>();
|
var apps = user.GetController<UIAppsController>();
|
||||||
var apps2 = user2.GetController<UIAppsController>();
|
var apps2 = user2.GetController<UIAppsController>();
|
||||||
var pos = user.GetController<UIPointOfSaleController>();
|
var pos = user.GetController<UIPointOfSaleController>();
|
||||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
var appType = PointOfSaleApp.AppType;
|
||||||
var appType = AppType.PointOfSale.ToString();
|
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
|
||||||
Assert.NotNull(vm.SelectedAppType);
|
Assert.Equal(appType, vm.SelectedAppType);
|
||||||
Assert.Null(vm.AppName);
|
Assert.Null(vm.AppName);
|
||||||
vm.AppName = "test";
|
vm.AppName = "test";
|
||||||
vm.SelectedAppType = appType;
|
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||||
Assert.Equal(nameof(pos.UpdatePointOfSale), redirectToAction.ActionName);
|
|
||||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
var appList2 =
|
var appList2 =
|
||||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||||
|
@ -1976,7 +1977,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||||
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
||||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
Assert.Empty(appList.Apps);
|
Assert.Empty(appList.Apps);
|
||||||
|
|
|
@ -27,20 +27,22 @@ public class AppSales : ViewComponent
|
||||||
|
|
||||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||||
{
|
{
|
||||||
var vm = new AppSalesViewModel()
|
var vm = new AppSalesViewModel
|
||||||
{
|
{
|
||||||
Id = appId,
|
Id = appId,
|
||||||
AppType = appType,
|
AppType = appType,
|
||||||
Url = Url.Action("AppSales", "UIApps", new { appId = appId }),
|
DataUrl = Url.Action("AppSales", "UIApps", new { appId }),
|
||||||
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
||||||
};
|
};
|
||||||
if (vm.InitialRendering)
|
if (vm.InitialRendering)
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
|
||||||
var app = HttpContext.GetAppData();
|
var app = HttpContext.GetAppData();
|
||||||
vm.AppType = app.AppType;
|
var stats = await _appService.GetSalesStats(app);
|
||||||
var stats = await _appService.GetSalesStats(HttpContext.GetAppData());
|
|
||||||
vm.SalesCount = stats.SalesCount;
|
vm.SalesCount = stats.SalesCount;
|
||||||
vm.Series = stats.Series;
|
vm.Series = stats.Series;
|
||||||
|
vm.AppType = app.AppType;
|
||||||
|
vm.AppUrl = await _appService.ConfigureLink(app, app.AppType);
|
||||||
|
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ public class AppSalesViewModel
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public AppSalesPeriod Period { get; set; }
|
public AppSalesPeriod Period { get; set; }
|
||||||
public string Url { get; set; }
|
public string AppUrl { 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<SalesStatsItem> Series { get; set; }
|
||||||
public bool InitialRendering { get; set; }
|
public bool InitialRendering { get; set; }
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
@using BTCPayServer.Services.Apps
|
|
||||||
@using BTCPayServer.Components.AppSales
|
@using BTCPayServer.Components.AppSales
|
||||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
@using BTCPayServer.Abstractions.TagHelpers
|
||||||
|
@using BTCPayServer.Plugins.Crowdfund
|
||||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||||
@{
|
@{
|
||||||
var controller = $"UI{Model.AppType}";
|
var label = Model.AppType == CrowdfundApp.AppType ? "Contributions" : "Sales";
|
||||||
var action = $"Update{Model.AppType}";
|
|
||||||
var label = Model.AppType == "Crowdfund" ? "Contributions" : "Sales";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div id="AppSales-@Model.Id" class="widget app-sales">
|
<div id="AppSales-@Model.Id" class="widget app-sales">
|
||||||
<header class="mb-3">
|
<header class="mb-3">
|
||||||
<h3>@Model.Name @label</h3>
|
<h3>@Model.Name @label</h3>
|
||||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.Id">Manage</a>
|
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||||
|
{
|
||||||
|
<a href="@Model.AppUrl">Manage</a>
|
||||||
|
}
|
||||||
</header>
|
</header>
|
||||||
@if (Model.InitialRendering)
|
@if (Model.InitialRendering)
|
||||||
{
|
{
|
||||||
|
@ -20,16 +21,16 @@
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
|
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
|
||||||
<script>
|
<script>
|
||||||
(async () => {
|
(async () => {
|
||||||
const url = @Safe.Json(Model.Url);
|
const url = @Safe.Json(Model.DataUrl);
|
||||||
const appId = @Safe.Json(Model.Id);
|
const appId = @Safe.Json(Model.Id);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
document.getElementById(`AppSales-${appId}`).outerHTML = await response.text();
|
document.getElementById(`AppSales-${appId}`).outerHTML = await response.text();
|
||||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -21,24 +21,22 @@ public class AppTopItems : ViewComponent
|
||||||
|
|
||||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType = null)
|
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType = null)
|
||||||
{
|
{
|
||||||
var vm = new AppTopItemsViewModel()
|
var vm = new AppTopItemsViewModel
|
||||||
{
|
{
|
||||||
Id = appId,
|
Id = appId,
|
||||||
AppType = appType,
|
AppType = appType,
|
||||||
Url = Url.Action("AppTopItems", "UIApps", new { appId = appId }),
|
DataUrl = Url.Action("AppTopItems", "UIApps", new { appId }),
|
||||||
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
||||||
};
|
};
|
||||||
if (vm.InitialRendering)
|
if (vm.InitialRendering)
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
|
||||||
var app = HttpContext.GetAppData();
|
var app = HttpContext.GetAppData();
|
||||||
vm.AppType = app.AppType;
|
var entries = await _appService.GetItemStats(app);
|
||||||
var entries = Enum.Parse<AppType>(vm.AppType) == AppType.Crowdfund
|
|
||||||
? await _appService.GetPerkStats(app)
|
|
||||||
: await _appService.GetItemStats(app);
|
|
||||||
|
|
||||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||||
vm.Entries = entries.ToList();
|
vm.Entries = entries.ToList();
|
||||||
|
vm.AppType = app.AppType;
|
||||||
|
vm.AppUrl = await _appService.ConfigureLink(app, app.AppType);
|
||||||
|
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ public class AppTopItemsViewModel
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public string Url { get; set; }
|
public string AppUrl { get; set; }
|
||||||
|
public string DataUrl { get; set; }
|
||||||
public List<ItemStats> Entries { get; set; }
|
public List<ItemStats> 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; }
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
@using BTCPayServer.Services.Apps
|
@using BTCPayServer.Plugins.Crowdfund
|
||||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
|
||||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||||
@{
|
@{
|
||||||
var controller = $"UI{Model.AppType}";
|
var label = Model.AppType == CrowdfundApp.AppType ? "contribution" : "sale";
|
||||||
var action = $"Update{Model.AppType}";
|
|
||||||
var label = Model.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
|
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
|
||||||
<header class="mb-3">
|
<header class="mb-3">
|
||||||
<h3>Top @(Model.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
|
<h3>Top @(Model.AppType == CrowdfundApp.AppType ? "Perks" : "Items")</h3>
|
||||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.Id">View All</a>
|
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||||
</header>
|
{
|
||||||
@if (Model.InitialRendering)
|
<a href="@Model.AppUrl">View All</a>
|
||||||
{
|
}
|
||||||
<div class="loading d-flex justify-content-center p-3">
|
</header>
|
||||||
<div class="spinner-border text-light" role="status">
|
@if (Model.InitialRendering)
|
||||||
<span class="visually-hidden">Loading...</span>
|
{
|
||||||
</div>
|
<div class="loading d-flex justify-content-center p-3">
|
||||||
</div>
|
<div class="spinner-border text-light" role="status">
|
||||||
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
|
<span class="visually-hidden">Loading...</span>
|
||||||
<script>
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
|
||||||
|
<script>
|
||||||
(async () => {
|
(async () => {
|
||||||
const url = @Safe.Json(Model.Url);
|
const url = @Safe.Json(Model.DataUrl);
|
||||||
const appId = @Safe.Json(Model.Id);
|
const appId = @Safe.Json(Model.Id);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
@ -31,35 +31,35 @@
|
||||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
else if (Model.Entries.Any())
|
else if (Model.Entries.Any())
|
||||||
{
|
{
|
||||||
<div class="ct-chart mb-3"></div>
|
<div class="ct-chart mb-3"></div>
|
||||||
<template>
|
<template>
|
||||||
@Safe.Json(Model)
|
@Safe.Json(Model)
|
||||||
</template>
|
</template>
|
||||||
<div class="app-items">
|
<div class="app-items">
|
||||||
@for (var i = 0; i < Model.Entries.Count; i++)
|
@for (var i = 0; i < Model.Entries.Count; i++)
|
||||||
{
|
{
|
||||||
var entry = Model.Entries[i];
|
var entry = Model.Entries[i];
|
||||||
<div class="app-item ct-series-@i">
|
<div class="app-item ct-series-@i">
|
||||||
<span class="app-item-name">
|
<span class="app-item-name">
|
||||||
<span class="app-item-point ct-point"></span>
|
<span class="app-item-point ct-point"></span>
|
||||||
@entry.Title
|
@entry.Title
|
||||||
</span>
|
</span>
|
||||||
<span class="app-item-value">
|
<span class="app-item-value">
|
||||||
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
||||||
@entry.TotalFormatted
|
@entry.TotalFormatted
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p class="text-secondary mt-3">
|
<p class="text-secondary mt-3">
|
||||||
No @($"{label}s") have been made yet.
|
No @($"{label}s") have been made yet.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -74,7 +74,7 @@ namespace BTCPayServer.Components.MainNav
|
||||||
Id = a.Id,
|
Id = a.Id,
|
||||||
IsOwner = a.IsOwner,
|
IsOwner = a.IsOwner,
|
||||||
AppName = a.AppName,
|
AppName = a.AppName,
|
||||||
AppType = Enum.Parse<AppType>(a.AppType)
|
AppType = a.AppType
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
if (PoliciesSettings.Experimental)
|
if (PoliciesSettings.Experimental)
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Components.MainNav
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string AppName { get; set; }
|
public string AppName { get; set; }
|
||||||
public AppType AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public bool IsOwner { get; set; }
|
public bool IsOwner { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers.Greenfield
|
namespace BTCPayServer.Controllers.Greenfield
|
||||||
{
|
{
|
||||||
|
@ -63,7 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
{
|
{
|
||||||
StoreDataId = storeId,
|
StoreDataId = storeId,
|
||||||
Name = request.AppName,
|
Name = request.AppName,
|
||||||
AppType = AppType.Crowdfund.ToString()
|
AppType = CrowdfundApp.AppType
|
||||||
};
|
};
|
||||||
|
|
||||||
appData.SetSettings(ToCrowdfundSettings(request));
|
appData.SetSettings(ToCrowdfundSettings(request));
|
||||||
|
@ -94,7 +97,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
{
|
{
|
||||||
StoreDataId = storeId,
|
StoreDataId = storeId,
|
||||||
Name = request.AppName,
|
Name = request.AppName,
|
||||||
AppType = AppType.PointOfSale.ToString()
|
AppType = PointOfSaleApp.AppType
|
||||||
};
|
};
|
||||||
|
|
||||||
appData.SetSettings(ToPointOfSaleSettings(request));
|
appData.SetSettings(ToPointOfSaleSettings(request));
|
||||||
|
@ -108,7 +111,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
|
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
{
|
{
|
||||||
return AppNotFound();
|
return AppNotFound();
|
||||||
|
@ -181,7 +184,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> GetPosApp(string appId)
|
public async Task<IActionResult> GetPosApp(string appId)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
{
|
{
|
||||||
return AppNotFound();
|
return AppNotFound();
|
||||||
|
@ -194,7 +197,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> GetCrowdfundApp(string appId)
|
public async Task<IActionResult> GetCrowdfundApp(string appId)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.Crowdfund);
|
var app = await _appService.GetApp(appId, CrowdfundApp.AppType);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
{
|
{
|
||||||
return AppNotFound();
|
return AppNotFound();
|
||||||
|
@ -264,7 +267,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
return new PointOfSaleSettings()
|
return new PointOfSaleSettings()
|
||||||
{
|
{
|
||||||
Title = request.Title,
|
Title = request.Title,
|
||||||
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
|
DefaultView = (PosViewType) request.DefaultView,
|
||||||
ShowCustomAmount = request.ShowCustomAmount,
|
ShowCustomAmount = request.ShowCustomAmount,
|
||||||
ShowDiscount = request.ShowDiscount,
|
ShowDiscount = request.ShowDiscount,
|
||||||
EnableTips = request.EnableTips,
|
EnableTips = request.EnableTips,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
@ -6,8 +5,6 @@ using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -58,12 +55,13 @@ namespace BTCPayServer.Controllers
|
||||||
if (app is null)
|
if (app is null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
return app.AppType switch
|
var res = await _appService.ViewLink(app);
|
||||||
|
if (res is null)
|
||||||
{
|
{
|
||||||
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
|
return NotFound();
|
||||||
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
|
}
|
||||||
_ => NotFound()
|
|
||||||
};
|
return Redirect(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
|
@ -114,12 +112,10 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
[HttpGet("/stores/{storeId}/apps/create")]
|
[HttpGet("/stores/{storeId}/apps/create")]
|
||||||
public IActionResult CreateApp(string storeId)
|
public IActionResult CreateApp(string storeId, string appType = null)
|
||||||
{
|
{
|
||||||
return View(new CreateAppViewModel
|
var vm = new CreateAppViewModel (_appService){StoreId = GetCurrentStore().Id, SelectedAppType = appType};
|
||||||
{
|
return View(vm);
|
||||||
StoreId = GetCurrentStore().Id
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
|
@ -128,8 +124,8 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
var store = GetCurrentStore();
|
var store = GetCurrentStore();
|
||||||
vm.StoreId = store.Id;
|
vm.StoreId = store.Id;
|
||||||
|
var types = _appService.GetAvailableAppTypes();
|
||||||
if (!Enum.TryParse(vm.SelectedAppType, out AppType appType))
|
if (!types.ContainsKey(vm.SelectedAppType))
|
||||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
|
@ -141,34 +137,18 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
StoreDataId = store.Id,
|
StoreDataId = store.Id,
|
||||||
Name = vm.AppName,
|
Name = vm.AppName,
|
||||||
AppType = appType.ToString()
|
AppType = vm.SelectedAppType
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
|
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
|
||||||
switch (appType)
|
await _appService.SetDefaultSettings(appData, defaultCurrency);
|
||||||
{
|
|
||||||
case AppType.Crowdfund:
|
|
||||||
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
|
|
||||||
appData.SetSettings(emptyCrowdfund);
|
|
||||||
break;
|
|
||||||
case AppType.PointOfSale:
|
|
||||||
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
|
|
||||||
appData.SetSettings(empty);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _appService.UpdateOrCreateApp(appData);
|
await _appService.UpdateOrCreateApp(appData);
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
||||||
CreatedAppId = appData.Id;
|
CreatedAppId = appData.Id;
|
||||||
|
|
||||||
return appType switch
|
var url = await _appService.ConfigureLink(appData, vm.SelectedAppType);
|
||||||
{
|
return Redirect(url);
|
||||||
AppType.PointOfSale => RedirectToAction(nameof(UIPointOfSaleController.UpdatePointOfSale), "UIPointOfSale", new { appId = appData.Id }),
|
|
||||||
AppType.Crowdfund => RedirectToAction(nameof(UICrowdfundController.UpdateCrowdfund), "UICrowdfund", new { appId = appData.Id }),
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
|
|
|
@ -213,7 +213,8 @@ namespace BTCPayServer.Controllers
|
||||||
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
|
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
[NonAction]
|
||||||
|
public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||||
{
|
{
|
||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||||
|
|
|
@ -18,6 +18,8 @@ using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
@ -47,7 +49,6 @@ namespace BTCPayServer
|
||||||
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
|
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
private readonly AppService _appService;
|
private readonly AppService _appService;
|
||||||
|
|
||||||
private readonly UIInvoiceController _invoiceController;
|
private readonly UIInvoiceController _invoiceController;
|
||||||
private readonly LinkGenerator _linkGenerator;
|
private readonly LinkGenerator _linkGenerator;
|
||||||
private readonly LightningAddressService _lightningAddressService;
|
private readonly LightningAddressService _lightningAddressService;
|
||||||
|
@ -155,6 +156,7 @@ namespace BTCPayServer
|
||||||
|
|
||||||
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
|
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
|
||||||
|
|
||||||
switch (claimResponse.PayoutData.State)
|
switch (claimResponse.PayoutData.State)
|
||||||
{
|
{
|
||||||
case PayoutState.AwaitingPayment:
|
case PayoutState.AwaitingPayment:
|
||||||
|
@ -249,37 +251,49 @@ namespace BTCPayServer
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewPointOfSaleViewModel.Item[] items = null;
|
ViewPointOfSaleViewModel.Item[] items;
|
||||||
string currencyCode = null;
|
string currencyCode;
|
||||||
|
PointOfSaleSettings posS = null;
|
||||||
switch (app.AppType)
|
switch (app.AppType)
|
||||||
{
|
{
|
||||||
case nameof(AppType.Crowdfund):
|
case CrowdfundApp.AppType:
|
||||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||||
currencyCode = cfS.TargetCurrency;
|
currencyCode = cfS.TargetCurrency;
|
||||||
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
||||||
break;
|
break;
|
||||||
case nameof(AppType.PointOfSale):
|
case PointOfSaleApp.AppType:
|
||||||
var posS = app.GetSettings<PointOfSaleSettings>();
|
posS = app.GetSettings<PointOfSaleSettings>();
|
||||||
currencyCode = posS.Currency;
|
currencyCode = posS.Currency;
|
||||||
items = _appService.Parse(posS.Template, posS.Currency);
|
items = _appService.Parse(posS.Template, posS.Currency);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
//TODO: Allow other apps to define lnurl support
|
||||||
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
ViewPointOfSaleViewModel.Item item = null;
|
||||||
var item = items.FirstOrDefault(item1 =>
|
if (!string.IsNullOrEmpty(itemCode))
|
||||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
{
|
||||||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
|
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
||||||
|
item = items.FirstOrDefault(item1 =>
|
||||||
|
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
if (item is null ||
|
if (item is null ||
|
||||||
item.Inventory <= 0 ||
|
item.Inventory <= 0 ||
|
||||||
(item.PaymentMethods?.Any() is true &&
|
(item.PaymentMethods?.Any() is true &&
|
||||||
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (app.AppType == PointOfSaleApp.AppType && posS?.ShowCustomAmount is not true)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
|
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
|
||||||
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item.Price.Value, true));
|
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item?.Price.Value, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EditLightningAddressVM
|
public class EditLightningAddressVM
|
||||||
|
@ -311,11 +325,8 @@ namespace BTCPayServer
|
||||||
public decimal? Max { get; set; }
|
public decimal? Max { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConcurrentDictionary<string, LightningAddressItem> Items { get; set; } =
|
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new ();
|
||||||
new ConcurrentDictionary<string, LightningAddressItem>();
|
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; } = new ();
|
||||||
|
|
||||||
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; set; } =
|
|
||||||
new ConcurrentDictionary<string, string[]>();
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
@ -389,7 +400,7 @@ namespace BTCPayServer
|
||||||
|
|
||||||
var redirectUrl = app?.AppType switch
|
var redirectUrl = app?.AppType switch
|
||||||
{
|
{
|
||||||
nameof(AppType.PointOfSale) => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
|
PointOfSaleApp.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
|
||||||
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
|
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,6 @@ using System.Web;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
|
||||||
using BTCPayServer.Plugins.PayButton.Models;
|
using BTCPayServer.Plugins.PayButton.Models;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
|
|
@ -347,7 +347,7 @@ namespace BTCPayServer.Controllers
|
||||||
if (appIdsToFetch.Any())
|
if (appIdsToFetch.Any())
|
||||||
{
|
{
|
||||||
var apps = (await _AppService.GetApps(appIdsToFetch.ToArray()))
|
var apps = (await _AppService.GetApps(appIdsToFetch.ToArray()))
|
||||||
.ToDictionary(data => data.Id, data => Enum.Parse<AppType>(data.AppType));
|
.ToDictionary(data => data.Id, data => data.AppType);
|
||||||
;
|
;
|
||||||
if (!string.IsNullOrEmpty(settings.RootAppId))
|
if (!string.IsNullOrEmpty(settings.RootAppId))
|
||||||
{
|
{
|
||||||
|
@ -422,8 +422,10 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
private async Task<List<SelectListItem>> GetAppSelectList()
|
private async Task<List<SelectListItem>> GetAppSelectList()
|
||||||
{
|
{
|
||||||
|
var types = _AppService.GetAvailableAppTypes();
|
||||||
var apps = (await _AppService.GetAllApps(null, true))
|
var apps = (await _AppService.GetAllApps(null, true))
|
||||||
.Select(a => new SelectListItem($"{typeof(AppType).DisplayName(a.AppType)} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
|
.Select(a =>
|
||||||
|
new SelectListItem($"{types[a.AppType]} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
|
||||||
apps.Insert(0, new SelectListItem("(None)", null));
|
apps.Insert(0, new SelectListItem("(None)", null));
|
||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,13 @@ namespace BTCPayServer.Filters
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public DomainMappingConstraintAttribute(AppType appType)
|
public DomainMappingConstraintAttribute(string appType)
|
||||||
{
|
{
|
||||||
AppType = appType;
|
AppType = appType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Order => 100;
|
public int Order => 100;
|
||||||
private AppType? AppType { get; }
|
private string AppType { get; }
|
||||||
|
|
||||||
public bool Accept(ActionConstraintContext context)
|
public bool Accept(ActionConstraintContext context)
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,9 +3,10 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Controllers;
|
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
|
||||||
namespace BTCPayServer.HostedServices
|
namespace BTCPayServer.HostedServices
|
||||||
|
@ -34,13 +35,13 @@ namespace BTCPayServer.HostedServices
|
||||||
//get all apps that were tagged that have manageable inventory that has an item that matches the item code in the invoice
|
//get all apps that were tagged that have manageable inventory that has an item that matches the item code in the invoice
|
||||||
var apps = (await _appService.GetApps(updateAppInventory.AppId)).Select(data =>
|
var apps = (await _appService.GetApps(updateAppInventory.AppId)).Select(data =>
|
||||||
{
|
{
|
||||||
switch (Enum.Parse<AppType>(data.AppType))
|
switch (data.AppType)
|
||||||
{
|
{
|
||||||
case AppType.PointOfSale:
|
case PointOfSaleApp.AppType:
|
||||||
var possettings = data.GetSettings<PointOfSaleSettings>();
|
var possettings = data.GetSettings<PointOfSaleSettings>();
|
||||||
return (Data: data, Settings: (object)possettings,
|
return (Data: data, Settings: (object)possettings,
|
||||||
Items: _appService.Parse(possettings.Template, possettings.Currency));
|
Items: _appService.Parse(possettings.Template, possettings.Currency));
|
||||||
case AppType.Crowdfund:
|
case CrowdfundApp.AppType:
|
||||||
var cfsettings = data.GetSettings<CrowdfundSettings>();
|
var cfsettings = data.GetSettings<CrowdfundSettings>();
|
||||||
return (Data: data, Settings: (object)cfsettings,
|
return (Data: data, Settings: (object)cfsettings,
|
||||||
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
|
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
|
||||||
|
@ -65,14 +66,13 @@ namespace BTCPayServer.HostedServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (Enum.Parse<AppType>(valueTuple.Data.AppType))
|
switch (valueTuple.Data.AppType)
|
||||||
{
|
{
|
||||||
case AppType.PointOfSale:
|
case PointOfSaleApp.AppType:
|
||||||
|
|
||||||
((PointOfSaleSettings)valueTuple.Settings).Template =
|
((PointOfSaleSettings)valueTuple.Settings).Template =
|
||||||
_appService.SerializeTemplate(valueTuple.Items);
|
_appService.SerializeTemplate(valueTuple.Items);
|
||||||
break;
|
break;
|
||||||
case AppType.Crowdfund:
|
case CrowdfundApp.AppType:
|
||||||
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
|
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
|
||||||
_appService.SerializeTemplate(valueTuple.Items);
|
_appService.SerializeTemplate(valueTuple.Items);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -7,36 +7,30 @@ using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Controllers;
|
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Fido2;
|
using BTCPayServer.Fido2;
|
||||||
using BTCPayServer.Fido2.Models;
|
using BTCPayServer.Fido2.Models;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Storage.Models;
|
using BTCPayServer.Storage.Models;
|
||||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||||
using ExchangeSharp;
|
|
||||||
using Fido2NetLib.Objects;
|
using Fido2NetLib.Objects;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
|
||||||
using NBitcoin.DataEncoders;
|
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using PeterO.Cbor;
|
using PeterO.Cbor;
|
||||||
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
|
||||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Hosting
|
namespace BTCPayServer.Hosting
|
||||||
{
|
{
|
||||||
|
@ -476,7 +470,7 @@ WHERE cte.""Id""=p.""Id""
|
||||||
string newTemplate;
|
string newTemplate;
|
||||||
switch (app.AppType)
|
switch (app.AppType)
|
||||||
{
|
{
|
||||||
case nameof(AppType.Crowdfund):
|
case CrowdfundApp.AppType:
|
||||||
var settings1 = app.GetSettings<CrowdfundSettings>();
|
var settings1 = app.GetSettings<CrowdfundSettings>();
|
||||||
if (string.IsNullOrEmpty(settings1.TargetCurrency))
|
if (string.IsNullOrEmpty(settings1.TargetCurrency))
|
||||||
{
|
{
|
||||||
|
@ -492,7 +486,7 @@ WHERE cte.""Id""=p.""Id""
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case nameof(AppType.PointOfSale):
|
case PointOfSaleApp.AppType:
|
||||||
|
|
||||||
var settings2 = app.GetSettings<PointOfSaleSettings>();
|
var settings2 = app.GetSettings<PointOfSaleSettings>();
|
||||||
if (string.IsNullOrEmpty(settings2.Currency))
|
if (string.IsNullOrEmpty(settings2.Currency))
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Data;
|
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
@ -11,17 +10,16 @@ namespace BTCPayServer.Models.AppViewModels
|
||||||
{
|
{
|
||||||
public CreateAppViewModel()
|
public CreateAppViewModel()
|
||||||
{
|
{
|
||||||
SetApps();
|
|
||||||
}
|
}
|
||||||
class Format
|
|
||||||
|
public CreateAppViewModel(AppService appService)
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
SetApps(appService);
|
||||||
public string Value { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
[MinLength(1)]
|
[MinLength(1)]
|
||||||
|
|
||||||
[Display(Name = "App Name")]
|
[Display(Name = "App Name")]
|
||||||
public string AppName { get; set; }
|
public string AppName { get; set; }
|
||||||
|
|
||||||
|
@ -33,16 +31,14 @@ namespace BTCPayServer.Models.AppViewModels
|
||||||
|
|
||||||
public SelectList AppTypes { get; set; }
|
public SelectList AppTypes { get; set; }
|
||||||
|
|
||||||
void SetApps()
|
private void SetApps(AppService appService)
|
||||||
{
|
{
|
||||||
var defaultAppType = AppType.PointOfSale.ToString();
|
var defaultAppType = PointOfSaleApp.AppType;
|
||||||
var choices = typeof(AppType).GetEnumNames().Select(o => new Format
|
var choices = appService.GetAvailableAppTypes().Select(pair =>
|
||||||
{
|
new SelectListItem(pair.Value, pair.Key, pair.Key == defaultAppType));
|
||||||
Name = typeof(AppType).DisplayName(o),
|
|
||||||
Value = o
|
|
||||||
}).ToArray();
|
|
||||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultAppType) ?? choices.FirstOrDefault();
|
var chosen = choices.FirstOrDefault(f => f.Value == defaultAppType) ?? choices.FirstOrDefault();
|
||||||
AppTypes = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
AppTypes = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Text), chosen);
|
||||||
SelectedAppType = chosen.Value;
|
SelectedAppType = chosen.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
|
|
||||||
namespace BTCPayServer.Models.AppViewModels
|
namespace BTCPayServer.Models.AppViewModels
|
||||||
|
@ -18,6 +19,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||||
public string UpdateAction { get { return "Update" + AppType; } }
|
public string UpdateAction { get { return "Update" + AppType; } }
|
||||||
public string ViewAction { get { return "View" + AppType; } }
|
public string ViewAction { get { return "View" + AppType; } }
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
|
public AppData App { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListAppViewModel[] Apps { get; set; }
|
public ListAppViewModel[] Apps { get; set; }
|
||||||
|
|
|
@ -19,20 +19,21 @@ namespace BTCPayServer.PaymentRequest
|
||||||
{
|
{
|
||||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||||
private readonly AppService _AppService;
|
private readonly InvoiceRepository _invoiceRepository;
|
||||||
private readonly CurrencyNameTable _currencies;
|
private readonly CurrencyNameTable _currencies;
|
||||||
private readonly DisplayFormatter _displayFormatter;
|
private readonly DisplayFormatter _displayFormatter;
|
||||||
|
|
||||||
public PaymentRequestService(
|
public PaymentRequestService(
|
||||||
PaymentRequestRepository paymentRequestRepository,
|
PaymentRequestRepository paymentRequestRepository,
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
InvoiceRepository invoiceRepository,
|
||||||
AppService appService,
|
AppService appService,
|
||||||
DisplayFormatter displayFormatter,
|
DisplayFormatter displayFormatter,
|
||||||
CurrencyNameTable currencies)
|
CurrencyNameTable currencies)
|
||||||
{
|
{
|
||||||
_PaymentRequestRepository = paymentRequestRepository;
|
_PaymentRequestRepository = paymentRequestRepository;
|
||||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
_AppService = appService;
|
_invoiceRepository = invoiceRepository;
|
||||||
_currencies = currencies;
|
_currencies = currencies;
|
||||||
_displayFormatter = displayFormatter;
|
_displayFormatter = displayFormatter;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +61,7 @@ namespace BTCPayServer.PaymentRequest
|
||||||
if (currentStatus != Client.Models.PaymentRequestData.PaymentRequestStatus.Expired)
|
if (currentStatus != Client.Models.PaymentRequestData.PaymentRequestStatus.Expired)
|
||||||
{
|
{
|
||||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||||
var contributions = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||||
|
|
||||||
currentStatus = contributions.TotalCurrency >= blob.Amount
|
currentStatus = contributions.TotalCurrency >= blob.Amount
|
||||||
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
|
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
|
||||||
|
@ -86,7 +87,7 @@ namespace BTCPayServer.PaymentRequest
|
||||||
|
|
||||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||||
|
|
||||||
var paymentStats = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||||
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
||||||
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
||||||
.FirstOrDefault(entity => entity.Status == InvoiceStatusLegacy.New);
|
.FirstOrDefault(entity => entity.Status == InvoiceStatusLegacy.New);
|
||||||
|
|
|
@ -35,11 +35,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
UIInvoiceController invoiceController,
|
UIInvoiceController invoiceController,
|
||||||
UserManager<ApplicationUser> userManager)
|
UserManager<ApplicationUser> userManager,
|
||||||
|
CrowdfundApp app)
|
||||||
{
|
{
|
||||||
_currencies = currencies;
|
_currencies = currencies;
|
||||||
_appService = appService;
|
_appService = appService;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_app = app;
|
||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_invoiceController = invoiceController;
|
_invoiceController = invoiceController;
|
||||||
|
@ -51,20 +53,21 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||||
private readonly AppService _appService;
|
private readonly AppService _appService;
|
||||||
private readonly UIInvoiceController _invoiceController;
|
private readonly UIInvoiceController _invoiceController;
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly CrowdfundApp _app;
|
||||||
|
|
||||||
[HttpGet("/")]
|
[HttpGet("/")]
|
||||||
[HttpGet("/apps/{appId}/crowdfund")]
|
[HttpGet("/apps/{appId}/crowdfund")]
|
||||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||||
[DomainMappingConstraint(AppType.Crowdfund)]
|
[DomainMappingConstraint(CrowdfundApp.AppType)]
|
||||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
public async Task<IActionResult> ViewCrowdfund(string appId)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.Crowdfund, true);
|
var app = await _appService.GetApp(appId, CrowdfundApp.AppType, true);
|
||||||
|
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var settings = app.GetSettings<CrowdfundSettings>();
|
var settings = app.GetSettings<CrowdfundSettings>();
|
||||||
|
|
||||||
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundApp.AppType) != null;
|
||||||
|
|
||||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
|
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
|
||||||
if (!hasEnoughSettingsToLoad)
|
if (!hasEnoughSettingsToLoad)
|
||||||
|
@ -89,17 +92,17 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||||
[IgnoreAntiforgeryToken]
|
[IgnoreAntiforgeryToken]
|
||||||
[EnableCors(CorsPolicies.All)]
|
[EnableCors(CorsPolicies.All)]
|
||||||
[DomainMappingConstraint(AppType.Crowdfund)]
|
[DomainMappingConstraint(CrowdfundApp.AppType)]
|
||||||
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
||||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.Crowdfund, true);
|
var app = await _appService.GetApp(appId, CrowdfundApp.AppType, true);
|
||||||
|
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var settings = app.GetSettings<CrowdfundSettings>();
|
var settings = app.GetSettings<CrowdfundSettings>();
|
||||||
|
|
||||||
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundApp.AppType) != null;
|
||||||
|
|
||||||
if (!settings.Enabled && !isAdmin)
|
if (!settings.Enabled && !isAdmin)
|
||||||
{
|
{
|
||||||
|
@ -395,7 +398,12 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||||
|
|
||||||
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
|
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
|
||||||
{
|
{
|
||||||
var info = (ViewCrowdfundViewModel)await _appService.GetAppInfo(appId);
|
var app = await _appService.GetApp(appId, CrowdfundApp.AppType, true);
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var info = (ViewCrowdfundViewModel) await _app.GetInfo(app);
|
||||||
info.HubPath = AppHub.GetHubPath(Request);
|
info.HubPath = AppHub.GetHubPath(Request);
|
||||||
info.SimpleDisplay = Request.Query.ContainsKey("simple");
|
info.SimpleDisplay = Request.Query.ContainsKey("simple");
|
||||||
return info;
|
return info;
|
||||||
|
|
|
@ -1,9 +1,24 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
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.Configuration;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Ganss.XSS;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.PayButton
|
namespace BTCPayServer.Plugins.Crowdfund
|
||||||
{
|
{
|
||||||
public class CrowdfundPlugin : BaseBTCPayServerPlugin
|
public class CrowdfundPlugin : BaseBTCPayServerPlugin
|
||||||
{
|
{
|
||||||
|
@ -14,7 +29,250 @@ namespace BTCPayServer.Plugins.PayButton
|
||||||
public override void Execute(IServiceCollection services)
|
public override void Execute(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IUIExtension>(new UIExtension("Crowdfund/NavExtension", "apps-nav"));
|
services.AddSingleton<IUIExtension>(new UIExtension("Crowdfund/NavExtension", "apps-nav"));
|
||||||
|
services.AddSingleton<CrowdfundApp>();
|
||||||
|
services.AddSingleton<IApp>(provider => provider.GetRequiredService<CrowdfundApp>());
|
||||||
|
|
||||||
base.Execute(services);
|
base.Execute(services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CrowdfundApp: IApp
|
||||||
|
{
|
||||||
|
private readonly LinkGenerator _linkGenerator;
|
||||||
|
private readonly IOptions<BTCPayServerOptions> _options;
|
||||||
|
private readonly DisplayFormatter _displayFormatter;
|
||||||
|
private readonly CurrencyNameTable _currencyNameTable;
|
||||||
|
private readonly HtmlSanitizer _htmlSanitizer;
|
||||||
|
private readonly InvoiceRepository _invoiceRepository;
|
||||||
|
public const string AppType = "Crowdfund";
|
||||||
|
public string Description => AppType;
|
||||||
|
public string Type => AppType;
|
||||||
|
public bool SupportsSalesStats => true;
|
||||||
|
public bool SupportsItemStats => true;
|
||||||
|
|
||||||
|
public CrowdfundApp(
|
||||||
|
LinkGenerator linkGenerator,
|
||||||
|
IOptions<BTCPayServerOptions> options,
|
||||||
|
InvoiceRepository invoiceRepository,
|
||||||
|
DisplayFormatter displayFormatter,
|
||||||
|
CurrencyNameTable currencyNameTable,
|
||||||
|
HtmlSanitizer htmlSanitizer)
|
||||||
|
{
|
||||||
|
_linkGenerator = linkGenerator;
|
||||||
|
_options = options;
|
||||||
|
_displayFormatter = displayFormatter;
|
||||||
|
_currencyNameTable = currencyNameTable;
|
||||||
|
_htmlSanitizer = htmlSanitizer;
|
||||||
|
_invoiceRepository = invoiceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> ConfigureLink(AppData app)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.UpdateCrowdfund),
|
||||||
|
"UICrowdfund", new { appId = app.Id }, _options.Value.RootPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||||
|
{
|
||||||
|
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||||
|
var items = AppService.Parse(_htmlSanitizer, _displayFormatter, cfS.PerksTemplate, cfS.TargetCurrency);
|
||||||
|
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
||||||
|
{
|
||||||
|
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||||
|
var perks = AppService.Parse(_htmlSanitizer, _displayFormatter, settings.PerksTemplate, settings.TargetCurrency);
|
||||||
|
var perkCount = paidInvoices
|
||||||
|
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
// we need the item code to know which perk it is and group by that
|
||||||
|
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||||
|
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||||
|
.Select(entities =>
|
||||||
|
{
|
||||||
|
var total = entities
|
||||||
|
.Sum(entity => entity.GetPayments(true)
|
||||||
|
.Sum(pay =>
|
||||||
|
{
|
||||||
|
var paymentMethodId = pay.GetPaymentMethodId();
|
||||||
|
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||||
|
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||||
|
return rate * value;
|
||||||
|
}));
|
||||||
|
var itemCode = entities.Key;
|
||||||
|
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
||||||
|
return new ItemStats
|
||||||
|
{
|
||||||
|
ItemCode = itemCode,
|
||||||
|
Title = perk?.Title ?? itemCode,
|
||||||
|
SalesCount = entities.Count(),
|
||||||
|
Total = total,
|
||||||
|
TotalFormatted = _displayFormatter.Currency(total, settings.TargetCurrency)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.OrderByDescending(stats => stats.SalesCount);
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<ItemStats>>(perkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> GetInfo(AppData appData)
|
||||||
|
{
|
||||||
|
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||||
|
var resetEvery = settings.StartDate.HasValue ? settings.ResetEvery : CrowdfundResetEvery.Never;
|
||||||
|
DateTime? lastResetDate = null;
|
||||||
|
DateTime? nextResetDate = null;
|
||||||
|
if (resetEvery != CrowdfundResetEvery.Never)
|
||||||
|
{
|
||||||
|
lastResetDate = settings.StartDate.Value;
|
||||||
|
|
||||||
|
nextResetDate = lastResetDate.Value;
|
||||||
|
while (DateTime.UtcNow >= nextResetDate)
|
||||||
|
{
|
||||||
|
lastResetDate = nextResetDate;
|
||||||
|
switch (resetEvery)
|
||||||
|
{
|
||||||
|
case CrowdfundResetEvery.Hour:
|
||||||
|
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
case CrowdfundResetEvery.Day:
|
||||||
|
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
case CrowdfundResetEvery.Month:
|
||||||
|
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
case CrowdfundResetEvery.Year:
|
||||||
|
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoices = await AppService.GetInvoicesForApp(_invoiceRepository,appData, lastResetDate);
|
||||||
|
var completeInvoices = invoices.Where(IsComplete).ToArray();
|
||||||
|
var pendingInvoices = invoices.Where(IsPending).ToArray();
|
||||||
|
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
||||||
|
|
||||||
|
var pendingPayments = _invoiceRepository.GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
||||||
|
var currentPayments = _invoiceRepository.GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
||||||
|
|
||||||
|
var perkCount = paidInvoices
|
||||||
|
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||||
|
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||||
|
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||||
|
|
||||||
|
Dictionary<string, decimal> perkValue = new();
|
||||||
|
if (settings.DisplayPerksValue)
|
||||||
|
{
|
||||||
|
perkValue = paidInvoices
|
||||||
|
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||||
|
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||||
|
.ToDictionary(entities => entities.Key, entities =>
|
||||||
|
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
||||||
|
{
|
||||||
|
var paymentMethodId = pay.GetPaymentMethodId();
|
||||||
|
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||||
|
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||||
|
return rate * value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
var perks = AppService.GetPOSItems(_htmlSanitizer, _displayFormatter, settings.PerksTemplate, settings.TargetCurrency);
|
||||||
|
if (settings.SortPerksByPopularity)
|
||||||
|
{
|
||||||
|
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
||||||
|
var newPerksOrder = ordered
|
||||||
|
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
|
||||||
|
.Where(matchingPerk => matchingPerk != null)
|
||||||
|
.ToList();
|
||||||
|
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
|
||||||
|
newPerksOrder.AddRange(remainingPerks);
|
||||||
|
perks = newPerksOrder.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = appData.StoreData;
|
||||||
|
var storeBlob = store.GetStoreBlob();
|
||||||
|
|
||||||
|
return new ViewCrowdfundViewModel
|
||||||
|
{
|
||||||
|
Title = settings.Title,
|
||||||
|
Tagline = settings.Tagline,
|
||||||
|
Description = settings.Description,
|
||||||
|
CustomCSSLink = settings.CustomCSSLink,
|
||||||
|
MainImageUrl = settings.MainImageUrl,
|
||||||
|
EmbeddedCSS = settings.EmbeddedCSS,
|
||||||
|
StoreName = store.StoreName,
|
||||||
|
CssFileId = storeBlob.CssFileId,
|
||||||
|
LogoFileId = storeBlob.LogoFileId,
|
||||||
|
BrandColor = storeBlob.BrandColor,
|
||||||
|
StoreId = appData.StoreDataId,
|
||||||
|
AppId = appData.Id,
|
||||||
|
StartDate = settings.StartDate?.ToUniversalTime(),
|
||||||
|
EndDate = settings.EndDate?.ToUniversalTime(),
|
||||||
|
TargetAmount = settings.TargetAmount,
|
||||||
|
TargetCurrency = settings.TargetCurrency,
|
||||||
|
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||||
|
Perks = perks,
|
||||||
|
Enabled = settings.Enabled,
|
||||||
|
DisqusEnabled = settings.DisqusEnabled,
|
||||||
|
SoundsEnabled = settings.SoundsEnabled,
|
||||||
|
DisqusShortname = settings.DisqusShortname,
|
||||||
|
AnimationsEnabled = settings.AnimationsEnabled,
|
||||||
|
ResetEveryAmount = settings.ResetEveryAmount,
|
||||||
|
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||||
|
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||||
|
PerkCount = perkCount,
|
||||||
|
PerkValue = perkValue,
|
||||||
|
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
|
||||||
|
Sounds = settings.Sounds,
|
||||||
|
AnimationColors = settings.AnimationColors,
|
||||||
|
CurrencyData = _currencyNameTable.GetCurrencyData(settings.TargetCurrency, true),
|
||||||
|
CurrencyDataPayments = currentPayments.Select(pair => pair.Key)
|
||||||
|
.Concat(pendingPayments.Select(pair => pair.Key))
|
||||||
|
.Select(id => _currencyNameTable.GetCurrencyData(id.CryptoCode, true)).DistinctBy(data => data.Code)
|
||||||
|
.ToDictionary(data => data.Code, data => data),
|
||||||
|
Info = new ViewCrowdfundViewModel.CrowdfundInfo
|
||||||
|
{
|
||||||
|
TotalContributors = paidInvoices.Length,
|
||||||
|
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
|
||||||
|
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
|
||||||
|
LastUpdated = DateTime.UtcNow,
|
||||||
|
PaymentStats = currentPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
||||||
|
PendingPaymentStats = pendingPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
||||||
|
LastResetDate = lastResetDate,
|
||||||
|
NextResetDate = nextResetDate,
|
||||||
|
CurrentPendingAmount = pendingPayments.TotalCurrency,
|
||||||
|
CurrentAmount = currentPayments.TotalCurrency
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||||
|
{
|
||||||
|
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
|
||||||
|
appData.SetSettings(emptyCrowdfund);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> ViewLink(AppData app)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.ViewCrowdfund),
|
||||||
|
"UICrowdfund", new {appId = app.Id}, _options.Value.RootPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPaid(InvoiceEntity entity)
|
||||||
|
{
|
||||||
|
return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed || entity.Status == InvoiceStatusLegacy.Paid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPending(InvoiceEntity entity)
|
||||||
|
{
|
||||||
|
return !(entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsComplete(InvoiceEntity entity)
|
||||||
|
{
|
||||||
|
return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,20 +60,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||||
public DateTime? LastResetDate { get; set; }
|
public DateTime? LastResetDate { get; set; }
|
||||||
public DateTime? NextResetDate { get; set; }
|
public DateTime? NextResetDate { get; set; }
|
||||||
}
|
}
|
||||||
public class Contribution
|
|
||||||
{
|
|
||||||
public PaymentMethodId PaymentMethodId { get; set; }
|
|
||||||
public decimal Value { get; set; }
|
|
||||||
public decimal CurrencyValue { get; set; }
|
|
||||||
}
|
|
||||||
public class Contributions : Dictionary<PaymentMethodId, Contribution>
|
|
||||||
{
|
|
||||||
public Contributions(IEnumerable<KeyValuePair<PaymentMethodId, Contribution>> collection) : base(collection)
|
|
||||||
{
|
|
||||||
TotalCurrency = Values.Select(v => v.CurrencyValue).Sum();
|
|
||||||
}
|
|
||||||
public decimal TotalCurrency { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Started => !StartDate.HasValue || DateTime.UtcNow > StartDate;
|
public bool Started => !StartDate.HasValue || DateTime.UtcNow > StartDate;
|
||||||
|
|
||||||
|
|
|
@ -64,11 +64,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
[HttpGet("/")]
|
[HttpGet("/")]
|
||||||
[HttpGet("/apps/{appId}/pos")]
|
[HttpGet("/apps/{appId}/pos")]
|
||||||
[HttpGet("/apps/{appId}/pos/{viewType?}")]
|
[HttpGet("/apps/{appId}/pos/{viewType?}")]
|
||||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
[DomainMappingConstraint(PointOfSaleApp.AppType)]
|
||||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||||
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
|
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||||
|
@ -121,7 +121,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
[HttpPost("/apps/{appId}/pos/{viewType?}")]
|
[HttpPost("/apps/{appId}/pos/{viewType?}")]
|
||||||
[IgnoreAntiforgeryToken]
|
[IgnoreAntiforgeryToken]
|
||||||
[EnableCors(CorsPolicies.All)]
|
[EnableCors(CorsPolicies.All)]
|
||||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
[DomainMappingConstraint(PointOfSaleApp.AppType)]
|
||||||
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
||||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||||
|
@ -137,7 +137,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
|
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
|
||||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||||
|
@ -334,7 +334,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||||
public async Task<IActionResult> POSForm(string appId, PosViewType? viewType = null)
|
public async Task<IActionResult> POSForm(string appId, PosViewType? viewType = null)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
@ -349,7 +349,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
var formParameters = Request.Form
|
var formParameters = Request.Form
|
||||||
.Where(pair => pair.Key != "__RequestVerificationToken")
|
.Where(pair => pair.Key != "__RequestVerificationToken")
|
||||||
.ToMultiValueDictionary(p => p.Key, p => p.Value.ToString());
|
.ToMultiValueDictionary(p => p.Key, p => p.Value.ToString());
|
||||||
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);;
|
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
|
||||||
var store = await _appService.GetStore(app);
|
var store = await _appService.GetStore(app);
|
||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
var form = Form.Parse(formData.Config);
|
var form = Form.Parse(formData.Config);
|
||||||
|
@ -380,7 +380,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||||
public async Task<IActionResult> POSFormSubmit(string appId, FormViewModel viewModel, PosViewType? viewType = null)
|
public async Task<IActionResult> POSFormSubmit(string appId, FormViewModel viewModel, PosViewType? viewType = null)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
@ -403,8 +403,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
|
|
||||||
if (FormDataService.Validate(form, ModelState))
|
if (FormDataService.Validate(form, ModelState))
|
||||||
{
|
{
|
||||||
|
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
|
||||||
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);;
|
|
||||||
var redirectUrl =
|
var redirectUrl =
|
||||||
Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new {appId, viewType}));
|
Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new {appId, viewType}));
|
||||||
formParameters.Add("formResponse", form.GetValues().ToString());
|
formParameters.Add("formResponse", form.GetValues().ToString());
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using BTCPayServer.Services.Apps;
|
|
||||||
using BTCPayServer.Services.Stores;
|
|
||||||
using BTCPayServer.Validation;
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
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.Configuration;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Ganss.XSS;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.PayButton
|
namespace BTCPayServer.Plugins.PointOfSale
|
||||||
{
|
{
|
||||||
public class PointOfSalePlugin : BaseBTCPayServerPlugin
|
public class PointOfSalePlugin : BaseBTCPayServerPlugin
|
||||||
{
|
{
|
||||||
|
@ -14,7 +28,108 @@ namespace BTCPayServer.Plugins.PayButton
|
||||||
public override void Execute(IServiceCollection services)
|
public override void Execute(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IUIExtension>(new UIExtension("PointOfSale/NavExtension", "apps-nav"));
|
services.AddSingleton<IUIExtension>(new UIExtension("PointOfSale/NavExtension", "apps-nav"));
|
||||||
|
services.AddSingleton<IApp,PointOfSaleApp>();
|
||||||
base.Execute(services);
|
base.Execute(services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PosViewType
|
||||||
|
{
|
||||||
|
[Display(Name = "Product list")]
|
||||||
|
Static,
|
||||||
|
[Display(Name = "Product list with cart")]
|
||||||
|
Cart,
|
||||||
|
[Display(Name = "Keypad only")]
|
||||||
|
Light,
|
||||||
|
[Display(Name = "Print display")]
|
||||||
|
Print
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PointOfSaleApp: IApp
|
||||||
|
{
|
||||||
|
private readonly LinkGenerator _linkGenerator;
|
||||||
|
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
||||||
|
private readonly DisplayFormatter _displayFormatter;
|
||||||
|
private readonly HtmlSanitizer _htmlSanitizer;
|
||||||
|
public const string AppType = "PointOfSale";
|
||||||
|
public string Description => "Point of Sale";
|
||||||
|
public string Type => AppType;
|
||||||
|
public bool SupportsSalesStats => true;
|
||||||
|
public bool SupportsItemStats => true;
|
||||||
|
|
||||||
|
public PointOfSaleApp(
|
||||||
|
LinkGenerator linkGenerator,
|
||||||
|
IOptions<BTCPayServerOptions> btcPayServerOptions,
|
||||||
|
DisplayFormatter displayFormatter,
|
||||||
|
HtmlSanitizer htmlSanitizer)
|
||||||
|
{
|
||||||
|
_linkGenerator = linkGenerator;
|
||||||
|
_btcPayServerOptions = btcPayServerOptions;
|
||||||
|
_displayFormatter = displayFormatter;
|
||||||
|
_htmlSanitizer = htmlSanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> ConfigureLink(AppData app)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIPointOfSaleController.UpdatePointOfSale),
|
||||||
|
"UIPointOfSale", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||||
|
{
|
||||||
|
var posS = app.GetSettings<PointOfSaleSettings>();
|
||||||
|
var items = AppService.Parse(_htmlSanitizer, _displayFormatter, posS.Template, posS.Currency);
|
||||||
|
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
||||||
|
{
|
||||||
|
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||||
|
var items = AppService.Parse(_htmlSanitizer, _displayFormatter, settings.Template, settings.Currency);
|
||||||
|
var itemCount = paidInvoices
|
||||||
|
.Where(entity => entity.Currency.Equals(settings.Currency, StringComparison.OrdinalIgnoreCase) && (
|
||||||
|
// The POS data is present for the cart view, where multiple items can be bought
|
||||||
|
entity.Metadata.PosData is not null ||
|
||||||
|
// The item code should be present for all types other than the cart and keypad
|
||||||
|
!string.IsNullOrEmpty(entity.Metadata.ItemCode)
|
||||||
|
))
|
||||||
|
.Aggregate(new List<AppService.InvoiceStatsItem>(), AppService.AggregateInvoiceEntitiesForStats(items))
|
||||||
|
.GroupBy(entity => entity.ItemCode)
|
||||||
|
.Select(entities =>
|
||||||
|
{
|
||||||
|
var total = entities.Sum(entity => entity.FiatPrice);
|
||||||
|
var itemCode = entities.Key;
|
||||||
|
var item = items.FirstOrDefault(p => p.Id == itemCode);
|
||||||
|
return new ItemStats
|
||||||
|
{
|
||||||
|
ItemCode = itemCode,
|
||||||
|
Title = item?.Title ?? itemCode,
|
||||||
|
SalesCount = entities.Count(),
|
||||||
|
Total = total,
|
||||||
|
TotalFormatted = _displayFormatter.Currency(total, settings.Currency)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.OrderByDescending(stats => stats.SalesCount);
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<ItemStats>>(itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<object> GetInfo(AppData appData)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||||
|
{
|
||||||
|
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
|
||||||
|
appData.SetSettings(empty);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> ViewLink(AppData app)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIPointOfSaleController.ViewPointOfSale),
|
||||||
|
"UIPointOfSale", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
private async Task InfoUpdated(string appId)
|
private async Task InfoUpdated(string appId)
|
||||||
{
|
{
|
||||||
var info = await _appService.GetAppInfo(appId);
|
var info = await _appService.GetInfo(appId);
|
||||||
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.InfoUpdated, new object[] { info });
|
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.InfoUpdated, new object[] { info });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,10 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
@ -25,13 +24,14 @@ using Newtonsoft.Json.Linq;
|
||||||
using YamlDotNet.Core;
|
using YamlDotNet.Core;
|
||||||
using YamlDotNet.RepresentationModel;
|
using YamlDotNet.RepresentationModel;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
using static BTCPayServer.Plugins.Crowdfund.Models.ViewCrowdfundViewModel;
|
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Apps
|
namespace BTCPayServer.Services.Apps
|
||||||
{
|
{
|
||||||
public class AppService
|
public class AppService
|
||||||
{
|
{
|
||||||
|
private readonly IEnumerable<IApp> _apps;
|
||||||
readonly ApplicationDbContextFactory _ContextFactory;
|
readonly ApplicationDbContextFactory _ContextFactory;
|
||||||
private readonly InvoiceRepository _InvoiceRepository;
|
private readonly InvoiceRepository _InvoiceRepository;
|
||||||
readonly CurrencyNameTable _Currencies;
|
readonly CurrencyNameTable _Currencies;
|
||||||
|
@ -39,13 +39,16 @@ namespace BTCPayServer.Services.Apps
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
private readonly HtmlSanitizer _HtmlSanitizer;
|
private readonly HtmlSanitizer _HtmlSanitizer;
|
||||||
public CurrencyNameTable Currencies => _Currencies;
|
public CurrencyNameTable Currencies => _Currencies;
|
||||||
public AppService(ApplicationDbContextFactory contextFactory,
|
public AppService(
|
||||||
InvoiceRepository invoiceRepository,
|
IEnumerable<IApp> apps,
|
||||||
CurrencyNameTable currencies,
|
ApplicationDbContextFactory contextFactory,
|
||||||
DisplayFormatter displayFormatter,
|
InvoiceRepository invoiceRepository,
|
||||||
StoreRepository storeRepository,
|
CurrencyNameTable currencies,
|
||||||
HtmlSanitizer htmlSanitizer)
|
DisplayFormatter displayFormatter,
|
||||||
|
StoreRepository storeRepository,
|
||||||
|
HtmlSanitizer htmlSanitizer)
|
||||||
{
|
{
|
||||||
|
_apps = apps;
|
||||||
_ContextFactory = contextFactory;
|
_ContextFactory = contextFactory;
|
||||||
_InvoiceRepository = invoiceRepository;
|
_InvoiceRepository = invoiceRepository;
|
||||||
_Currencies = currencies;
|
_Currencies = currencies;
|
||||||
|
@ -54,251 +57,53 @@ namespace BTCPayServer.Services.Apps
|
||||||
_displayFormatter = displayFormatter;
|
_displayFormatter = displayFormatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> GetAppInfo(string appId)
|
public Dictionary<string, string> GetAvailableAppTypes()
|
||||||
{
|
{
|
||||||
var app = await GetApp(appId, AppType.Crowdfund, true);
|
return _apps.ToDictionary(app => app.Type, app => app.Description);
|
||||||
if (app != null)
|
|
||||||
{
|
|
||||||
return await GetInfo(app);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData)
|
public Task<string> ConfigureLink(AppData app, string vmSelectedAppType)
|
||||||
{
|
{
|
||||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
return GetAppForType(vmSelectedAppType).ConfigureLink(app);
|
||||||
var resetEvery = settings.StartDate.HasValue ? settings.ResetEvery : CrowdfundResetEvery.Never;
|
|
||||||
DateTime? lastResetDate = null;
|
|
||||||
DateTime? nextResetDate = null;
|
|
||||||
if (resetEvery != CrowdfundResetEvery.Never)
|
|
||||||
{
|
|
||||||
lastResetDate = settings.StartDate.Value;
|
|
||||||
|
|
||||||
nextResetDate = lastResetDate.Value;
|
|
||||||
while (DateTime.UtcNow >= nextResetDate)
|
|
||||||
{
|
|
||||||
lastResetDate = nextResetDate;
|
|
||||||
switch (resetEvery)
|
|
||||||
{
|
|
||||||
case CrowdfundResetEvery.Hour:
|
|
||||||
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
case CrowdfundResetEvery.Day:
|
|
||||||
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
case CrowdfundResetEvery.Month:
|
|
||||||
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
case CrowdfundResetEvery.Year:
|
|
||||||
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoices = await GetInvoicesForApp(appData, lastResetDate);
|
|
||||||
var completeInvoices = invoices.Where(IsComplete).ToArray();
|
|
||||||
var pendingInvoices = invoices.Where(IsPending).ToArray();
|
|
||||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
|
||||||
|
|
||||||
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
|
||||||
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
|
||||||
|
|
||||||
var perkCount = paidInvoices
|
|
||||||
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
|
||||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
|
||||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
|
||||||
|
|
||||||
Dictionary<string, decimal> perkValue = new();
|
|
||||||
if (settings.DisplayPerksValue)
|
|
||||||
{
|
|
||||||
perkValue = paidInvoices
|
|
||||||
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
|
||||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
|
||||||
.ToDictionary(entities => entities.Key, entities =>
|
|
||||||
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
|
||||||
{
|
|
||||||
var paymentMethodId = pay.GetPaymentMethodId();
|
|
||||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
|
||||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
|
||||||
return rate * value;
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
|
||||||
if (settings.SortPerksByPopularity)
|
|
||||||
{
|
|
||||||
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
|
||||||
var newPerksOrder = ordered
|
|
||||||
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
|
|
||||||
.Where(matchingPerk => matchingPerk != null)
|
|
||||||
.ToList();
|
|
||||||
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
|
|
||||||
newPerksOrder.AddRange(remainingPerks);
|
|
||||||
perks = newPerksOrder.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
var store = appData.StoreData;
|
|
||||||
var storeBlob = store.GetStoreBlob();
|
|
||||||
|
|
||||||
return new ViewCrowdfundViewModel
|
|
||||||
{
|
|
||||||
Title = settings.Title,
|
|
||||||
Tagline = settings.Tagline,
|
|
||||||
Description = settings.Description,
|
|
||||||
CustomCSSLink = settings.CustomCSSLink,
|
|
||||||
MainImageUrl = settings.MainImageUrl,
|
|
||||||
EmbeddedCSS = settings.EmbeddedCSS,
|
|
||||||
StoreName = store.StoreName,
|
|
||||||
CssFileId = storeBlob.CssFileId,
|
|
||||||
LogoFileId = storeBlob.LogoFileId,
|
|
||||||
BrandColor = storeBlob.BrandColor,
|
|
||||||
StoreId = appData.StoreDataId,
|
|
||||||
AppId = appData.Id,
|
|
||||||
StartDate = settings.StartDate?.ToUniversalTime(),
|
|
||||||
EndDate = settings.EndDate?.ToUniversalTime(),
|
|
||||||
TargetAmount = settings.TargetAmount,
|
|
||||||
TargetCurrency = settings.TargetCurrency,
|
|
||||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
|
||||||
Perks = perks,
|
|
||||||
Enabled = settings.Enabled,
|
|
||||||
DisqusEnabled = settings.DisqusEnabled,
|
|
||||||
SoundsEnabled = settings.SoundsEnabled,
|
|
||||||
DisqusShortname = settings.DisqusShortname,
|
|
||||||
AnimationsEnabled = settings.AnimationsEnabled,
|
|
||||||
ResetEveryAmount = settings.ResetEveryAmount,
|
|
||||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
|
||||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
|
||||||
PerkCount = perkCount,
|
|
||||||
PerkValue = perkValue,
|
|
||||||
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
|
|
||||||
Sounds = settings.Sounds,
|
|
||||||
AnimationColors = settings.AnimationColors,
|
|
||||||
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
|
||||||
CurrencyDataPayments = Enumerable.DistinctBy(currentPayments.Select(pair => pair.Key)
|
|
||||||
.Concat(pendingPayments.Select(pair => pair.Key))
|
|
||||||
.Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true)), data => data.Code)
|
|
||||||
.ToDictionary(data => data.Code, data => data),
|
|
||||||
Info = new CrowdfundInfo
|
|
||||||
{
|
|
||||||
TotalContributors = paidInvoices.Length,
|
|
||||||
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
|
|
||||||
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
|
|
||||||
LastUpdated = DateTime.UtcNow,
|
|
||||||
PaymentStats = currentPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
|
||||||
PendingPaymentStats = pendingPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
|
||||||
LastResetDate = lastResetDate,
|
|
||||||
NextResetDate = nextResetDate,
|
|
||||||
CurrentPendingAmount = pendingPayments.TotalCurrency,
|
|
||||||
CurrentAmount = currentPayments.TotalCurrency
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsPending(InvoiceEntity entity)
|
private IApp GetAppForType(string appType)
|
||||||
{
|
{
|
||||||
return !(entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed);
|
return _apps.First(app => app.Type == appType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsComplete(InvoiceEntity entity)
|
public async Task<object> GetInfo(string appId)
|
||||||
{
|
{
|
||||||
return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed;
|
var appData = await GetApp(appId, null);
|
||||||
}
|
if (appData is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var app = GetAppForType(appData.AppType);
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ItemStats>> GetPerkStats(AppData appData)
|
return app.GetInfo(appData);
|
||||||
{
|
|
||||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
|
||||||
var invoices = await GetInvoicesForApp(appData);
|
|
||||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
|
||||||
var currencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true);
|
|
||||||
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
|
||||||
var perkCount = paidInvoices
|
|
||||||
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
// we need the item code to know which perk it is and group by that
|
|
||||||
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
|
||||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
|
||||||
.Select(entities =>
|
|
||||||
{
|
|
||||||
var total = entities
|
|
||||||
.Sum(entity => entity.GetPayments(true)
|
|
||||||
.Sum(pay =>
|
|
||||||
{
|
|
||||||
var paymentMethodId = pay.GetPaymentMethodId();
|
|
||||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
|
||||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
|
||||||
return rate * value;
|
|
||||||
}));
|
|
||||||
var itemCode = entities.Key;
|
|
||||||
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
|
||||||
return new ItemStats
|
|
||||||
{
|
|
||||||
ItemCode = itemCode,
|
|
||||||
Title = perk?.Title ?? itemCode,
|
|
||||||
SalesCount = entities.Count(),
|
|
||||||
Total = total,
|
|
||||||
TotalFormatted = $"{total.ShowMoney(currencyData.Divisibility)} {settings.TargetCurrency}"
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.OrderByDescending(stats => stats.SalesCount);
|
|
||||||
|
|
||||||
return perkCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData appData)
|
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData appData)
|
||||||
{
|
{
|
||||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository,appData,
|
||||||
var invoices = await GetInvoicesForApp(appData);
|
null, new []
|
||||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
|
||||||
var currencyData = _Currencies.GetCurrencyData(settings.Currency, true);
|
|
||||||
var items = Parse(settings.Template, settings.Currency);
|
|
||||||
var itemCount = paidInvoices
|
|
||||||
.Where(entity => entity.Currency.Equals(settings.Currency, StringComparison.OrdinalIgnoreCase) && (
|
|
||||||
// The POS data is present for the cart view, where multiple items can be bought
|
|
||||||
entity.Metadata.PosData != null ||
|
|
||||||
// The item code should be present for all types other than the cart and keypad
|
|
||||||
!string.IsNullOrEmpty(entity.Metadata.ItemCode)
|
|
||||||
))
|
|
||||||
.Aggregate(new List<InvoiceStatsItem>(), AggregateInvoiceEntitiesForStats(items))
|
|
||||||
.GroupBy(entity => entity.ItemCode)
|
|
||||||
.Select(entities =>
|
|
||||||
{
|
{
|
||||||
var total = entities.Sum(entity => entity.FiatPrice);
|
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
|
||||||
var itemCode = entities.Key;
|
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
|
||||||
var item = items.FirstOrDefault(p => p.Id == itemCode);
|
InvoiceState.ToString(InvoiceStatusLegacy.Complete)
|
||||||
return new ItemStats
|
});
|
||||||
{
|
return await GetAppForType(appData.AppType).GetItemStats(appData, paidInvoices);
|
||||||
ItemCode = itemCode,
|
|
||||||
Title = item?.Title ?? itemCode,
|
|
||||||
SalesCount = entities.Count(),
|
|
||||||
Total = total,
|
|
||||||
TotalFormatted = $"{total.ShowMoney(currencyData.Divisibility)} {settings.Currency}"
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.OrderByDescending(stats => stats.SalesCount);
|
|
||||||
|
|
||||||
return itemCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SalesStats> GetSalesStats(AppData app, int numberOfDays = 7)
|
public static Task<SalesStats> GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items,
|
||||||
|
InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||||
{
|
{
|
||||||
ViewPointOfSaleViewModel.Item[] items = null;
|
|
||||||
switch (app.AppType)
|
|
||||||
{
|
|
||||||
case nameof(AppType.Crowdfund):
|
|
||||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
|
||||||
items = Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
|
||||||
break;
|
|
||||||
case nameof(AppType.PointOfSale):
|
|
||||||
var posS = app.GetSettings<PointOfSaleSettings>();
|
|
||||||
items = Parse(posS.Template, posS.Currency);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoices = await GetInvoicesForApp(app);
|
|
||||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
|
||||||
var series = paidInvoices
|
var series = paidInvoices
|
||||||
.Where(entity => entity.InvoiceTime > DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays))
|
|
||||||
.Aggregate(new List<InvoiceStatsItem>(), AggregateInvoiceEntitiesForStats(items))
|
.Aggregate(new List<InvoiceStatsItem>(), AggregateInvoiceEntitiesForStats(items))
|
||||||
.GroupBy(entity => entity.Date)
|
.GroupBy(entity => entity.Date)
|
||||||
.Select(entities => new SalesStatsItem
|
.Select(entities => new SalesStatsItem
|
||||||
|
@ -322,21 +127,33 @@ namespace BTCPayServer.Services.Apps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SalesStats
|
return Task.FromResult(new SalesStats
|
||||||
{
|
{
|
||||||
SalesCount = series.Sum(i => i.SalesCount),
|
SalesCount = series.Sum(i => i.SalesCount),
|
||||||
Series = series.OrderBy(i => i.Label)
|
Series = series.OrderBy(i => i.Label)
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InvoiceStatsItem
|
public async Task<SalesStats> GetSalesStats(AppData app, int numberOfDays = 7)
|
||||||
|
{
|
||||||
|
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays),
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
|
||||||
|
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
|
||||||
|
InvoiceState.ToString(InvoiceStatusLegacy.Complete)
|
||||||
|
});
|
||||||
|
return await GetAppForType(app.AppType).GetSalesStats(app, paidInvoices, numberOfDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvoiceStatsItem
|
||||||
{
|
{
|
||||||
public string ItemCode { get; set; }
|
public string ItemCode { get; set; }
|
||||||
public decimal FiatPrice { get; set; }
|
public decimal FiatPrice { get; set; }
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Func<List<InvoiceStatsItem>, InvoiceEntity, List<InvoiceStatsItem>> AggregateInvoiceEntitiesForStats(ViewPointOfSaleViewModel.Item[] items)
|
public static Func<List<InvoiceStatsItem>, InvoiceEntity, List<InvoiceStatsItem>> AggregateInvoiceEntitiesForStats(ViewPointOfSaleViewModel.Item[] items)
|
||||||
{
|
{
|
||||||
return (res, e) =>
|
return (res, e) =>
|
||||||
{
|
{
|
||||||
|
@ -383,17 +200,13 @@ namespace BTCPayServer.Services.Apps
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsPaid(InvoiceEntity entity)
|
public static string GetAppOrderId(AppData app) => GetAppOrderId(app.AppType, app.Id);
|
||||||
{
|
public static string GetAppOrderId(string appType, string appId) =>
|
||||||
return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed || entity.Status == InvoiceStatusLegacy.Paid;
|
appType switch
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetAppOrderId(AppData app) =>
|
|
||||||
app.AppType switch
|
|
||||||
{
|
{
|
||||||
nameof(AppType.Crowdfund) => $"crowdfund-app_{app.Id}",
|
CrowdfundApp.AppType => $"crowdfund-app_{appId}",
|
||||||
nameof(AppType.PointOfSale) => $"pos-app_{app.Id}",
|
PointOfSaleApp.AppType => $"pos-app_{appId}",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.AppType)
|
_ => $"{appType}_{appId}"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
||||||
|
@ -402,13 +215,13 @@ namespace BTCPayServer.Services.Apps
|
||||||
return invoice.GetInternalTags("APP#");
|
return invoice.GetInternalTags("APP#");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
public static async Task<InvoiceEntity[]> GetInvoicesForApp(InvoiceRepository invoiceRepository, AppData appData, DateTimeOffset? startDate = null, string[] status = null)
|
||||||
{
|
{
|
||||||
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
var invoices = await invoiceRepository.GetInvoices(new InvoiceQuery
|
||||||
{
|
{
|
||||||
StoreId = new[] { appData.StoreData.Id },
|
StoreId = new[] { appData.StoreDataId },
|
||||||
OrderId = appData.TagAllInvoices ? null : new[] { GetAppOrderId(appData) },
|
OrderId = appData.TagAllInvoices ? null : new[] { GetAppOrderId(appData) },
|
||||||
Status = new[]{
|
Status = status?? new[]{
|
||||||
InvoiceState.ToString(InvoiceStatusLegacy.New),
|
InvoiceState.ToString(InvoiceStatusLegacy.New),
|
||||||
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
|
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
|
||||||
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
|
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
|
||||||
|
@ -424,7 +237,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
public async Task<StoreData[]> GetOwnedStores(string userId)
|
public async Task<StoreData[]> GetOwnedStores(string userId)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
return await ctx.UserStore
|
return await ctx.UserStore
|
||||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||||
.Select(u => u.StoreData)
|
.Select(u => u.StoreData)
|
||||||
|
@ -433,7 +246,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
public async Task<bool> DeleteApp(AppData appData)
|
public async Task<bool> DeleteApp(AppData appData)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
ctx.Apps.Add(appData);
|
ctx.Apps.Add(appData);
|
||||||
ctx.Entry(appData).State = EntityState.Deleted;
|
ctx.Entry(appData).State = EntityState.Deleted;
|
||||||
return await ctx.SaveChangesAsync() == 1;
|
return await ctx.SaveChangesAsync() == 1;
|
||||||
|
@ -441,7 +254,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false, string storeId = null)
|
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false, string storeId = null)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
var listApps = await ctx.UserStore
|
var listApps = await ctx.UserStore
|
||||||
.Where(us =>
|
.Where(us =>
|
||||||
(allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) &&
|
(allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) &&
|
||||||
|
@ -457,6 +270,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
AppType = app.AppType,
|
AppType = app.AppType,
|
||||||
Id = app.Id,
|
Id = app.Id,
|
||||||
Created = app.Created,
|
Created = app.Created,
|
||||||
|
App = app
|
||||||
})
|
})
|
||||||
.OrderBy(b => b.Created)
|
.OrderBy(b => b.Created)
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
@ -469,28 +283,23 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
foreach (ListAppsViewModel.ListAppViewModel app in listApps)
|
foreach (ListAppsViewModel.ListAppViewModel app in listApps)
|
||||||
{
|
{
|
||||||
app.ViewStyle = await GetAppViewStyleAsync(app.Id, app.AppType);
|
app.ViewStyle = GetAppViewStyle(app.App, app.AppType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return listApps;
|
return listApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetAppViewStyleAsync(string appId, string appType)
|
public string GetAppViewStyle(AppData app, string appType)
|
||||||
{
|
{
|
||||||
AppType appTypeEnum = Enum.Parse<AppType>(appType);
|
|
||||||
AppData appData = await GetApp(appId, appTypeEnum, false);
|
|
||||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
|
||||||
|
|
||||||
string style;
|
string style;
|
||||||
switch (appTypeEnum)
|
switch (appType)
|
||||||
{
|
{
|
||||||
case AppType.PointOfSale:
|
case PointOfSaleApp.AppType:
|
||||||
|
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||||
string posViewStyle = (settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView).ToString();
|
string posViewStyle = (settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView).ToString();
|
||||||
style = typeof(PosViewType).DisplayName(posViewStyle);
|
style = typeof(PosViewType).DisplayName(posViewStyle);
|
||||||
break;
|
break;
|
||||||
case AppType.Crowdfund:
|
|
||||||
style = string.Empty;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
style = string.Empty;
|
style = string.Empty;
|
||||||
break;
|
break;
|
||||||
|
@ -501,10 +310,9 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false)
|
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
var query = ctx.Apps
|
var query = ctx.Apps
|
||||||
.Where(us => appIds.Contains(us.Id));
|
.Where(app => appIds.Contains(app.Id));
|
||||||
|
|
||||||
if (includeStore)
|
if (includeStore)
|
||||||
{
|
{
|
||||||
query = query.Include(data => data.StoreData);
|
query = query.Include(data => data.StoreData);
|
||||||
|
@ -512,12 +320,20 @@ namespace BTCPayServer.Services.Apps
|
||||||
return await query.ToListAsync();
|
return await query.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AppData> GetApp(string appId, AppType? appType, bool includeStore = false)
|
public async Task<List<AppData>> GetApps(string appType)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
var query = ctx.Apps
|
||||||
|
.Where(app => app.AppType == appType);
|
||||||
|
return await query.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppData> GetApp(string appId, string appType, bool includeStore = false)
|
||||||
|
{
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
var query = ctx.Apps
|
var query = ctx.Apps
|
||||||
.Where(us => us.Id == appId &&
|
.Where(us => us.Id == appId &&
|
||||||
(appType == null || us.AppType == appType.ToString()));
|
(appType == null || us.AppType == appType));
|
||||||
|
|
||||||
if (includeStore)
|
if (includeStore)
|
||||||
{
|
{
|
||||||
|
@ -573,21 +389,32 @@ namespace BTCPayServer.Services.Apps
|
||||||
var serializer = new SerializerBuilder().Build();
|
var serializer = new SerializerBuilder().Build();
|
||||||
return serializer.Serialize(mappingNode);
|
return serializer.Serialize(mappingNode);
|
||||||
}
|
}
|
||||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
|
||||||
|
public ViewPointOfSaleViewModel.Item[] Parse( string template, string currency)
|
||||||
|
{
|
||||||
|
return Parse(_HtmlSanitizer, _displayFormatter, template, currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ViewPointOfSaleViewModel.Item[] GetPOSItems(string template, string currency)
|
||||||
|
{
|
||||||
|
return GetPOSItems(_HtmlSanitizer, _displayFormatter, template, currency);
|
||||||
|
}
|
||||||
|
public static ViewPointOfSaleViewModel.Item[] Parse(HtmlSanitizer htmlSanitizer, DisplayFormatter displayFormatter, string template, string currency)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(template))
|
if (string.IsNullOrWhiteSpace(template))
|
||||||
return Array.Empty<ViewPointOfSaleViewModel.Item>();
|
return Array.Empty<ViewPointOfSaleViewModel.Item>();
|
||||||
using var input = new StringReader(template);
|
using var input = new StringReader(template);
|
||||||
YamlStream stream = new YamlStream();
|
YamlStream stream = new ();
|
||||||
stream.Load(input);
|
stream.Load(input);
|
||||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||||
return root
|
return root
|
||||||
.Children
|
.Children
|
||||||
.Select(kv => new PosHolder(_HtmlSanitizer) { Key = _HtmlSanitizer.Sanitize((kv.Key as YamlScalarNode)?.Value), Value = kv.Value as YamlMappingNode })
|
.Select(kv => new PosHolder(htmlSanitizer) { Key = htmlSanitizer.Sanitize((kv.Key as YamlScalarNode)?.Value), Value = kv.Value as YamlMappingNode })
|
||||||
.Where(kv => kv.Value != null)
|
.Where(kv => kv.Value != null)
|
||||||
.Select(c =>
|
.Select(c =>
|
||||||
{
|
{
|
||||||
ViewPointOfSaleViewModel.Item.ItemPrice price = new ViewPointOfSaleViewModel.Item.ItemPrice();
|
ViewPointOfSaleViewModel.Item.ItemPrice price = new ();
|
||||||
var pValue = c.GetDetail("price")?.FirstOrDefault();
|
var pValue = c.GetDetail("price")?.FirstOrDefault();
|
||||||
|
|
||||||
switch (c.GetDetailString("custom") ?? c.GetDetailString("price_type")?.ToLowerInvariant())
|
switch (c.GetDetailString("custom") ?? c.GetDetailString("price_type")?.ToLowerInvariant())
|
||||||
|
@ -599,10 +426,10 @@ namespace BTCPayServer.Services.Apps
|
||||||
case "true":
|
case "true":
|
||||||
case "minimum":
|
case "minimum":
|
||||||
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum;
|
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum;
|
||||||
if (pValue != null)
|
if (pValue != null && !string.IsNullOrEmpty(pValue.Value?.Value))
|
||||||
{
|
{
|
||||||
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
|
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
|
||||||
price.Formatted = _displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "fixed":
|
case "fixed":
|
||||||
|
@ -610,11 +437,11 @@ namespace BTCPayServer.Services.Apps
|
||||||
case null:
|
case null:
|
||||||
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
|
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
|
||||||
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
|
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
|
||||||
price.Formatted = _displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ViewPointOfSaleViewModel.Item()
|
return new ViewPointOfSaleViewModel.Item
|
||||||
{
|
{
|
||||||
Description = c.GetDetailString("description"),
|
Description = c.GetDetailString("description"),
|
||||||
Id = c.Key,
|
Id = c.Key,
|
||||||
|
@ -624,7 +451,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
BuyButtonText = c.GetDetailString("buyButtonText"),
|
BuyButtonText = c.GetDetailString("buyButtonText"),
|
||||||
Inventory =
|
Inventory =
|
||||||
string.IsNullOrEmpty(c.GetDetailString("inventory"))
|
string.IsNullOrEmpty(c.GetDetailString("inventory"))
|
||||||
? (int?)null
|
? null
|
||||||
: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
|
: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
|
||||||
PaymentMethods = c.GetDetailStringList("payment_methods"),
|
PaymentMethods = c.GetDetailStringList("payment_methods"),
|
||||||
Disabled = c.GetDetailString("disabled") == "true"
|
Disabled = c.GetDetailString("disabled") == "true"
|
||||||
|
@ -633,65 +460,11 @@ namespace BTCPayServer.Services.Apps
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ViewPointOfSaleViewModel.Item[] GetPOSItems(string template, string currency)
|
public static ViewPointOfSaleViewModel.Item[] GetPOSItems(HtmlSanitizer htmlSanitizer, DisplayFormatter displayFormatter, string template, string currency)
|
||||||
{
|
{
|
||||||
return Parse(template, currency).Where(c => !c.Disabled).ToArray();
|
return Parse(htmlSanitizer, displayFormatter, template, currency).Where(c => !c.Disabled).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contributions GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
|
|
||||||
{
|
|
||||||
var contributions = invoices
|
|
||||||
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.SelectMany(p =>
|
|
||||||
{
|
|
||||||
var contribution = new Contribution();
|
|
||||||
contribution.PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike);
|
|
||||||
contribution.CurrencyValue = p.Price;
|
|
||||||
contribution.Value = contribution.CurrencyValue;
|
|
||||||
|
|
||||||
// For hardcap, we count newly created invoices as part of the contributions
|
|
||||||
if (!softcap && p.Status == InvoiceStatusLegacy.New)
|
|
||||||
return new[] { contribution };
|
|
||||||
|
|
||||||
// If the user get a donation via other mean, he can register an invoice manually for such amount
|
|
||||||
// then mark the invoice as complete
|
|
||||||
var payments = p.GetPayments(true);
|
|
||||||
if (payments.Count == 0 &&
|
|
||||||
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
|
||||||
p.Status == InvoiceStatusLegacy.Complete)
|
|
||||||
return new[] { contribution };
|
|
||||||
|
|
||||||
contribution.CurrencyValue = 0m;
|
|
||||||
contribution.Value = 0m;
|
|
||||||
|
|
||||||
// If an invoice has been marked invalid, remove the contribution
|
|
||||||
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
|
||||||
p.Status == InvoiceStatusLegacy.Invalid)
|
|
||||||
return new[] { contribution };
|
|
||||||
|
|
||||||
|
|
||||||
// Else, we just sum the payments
|
|
||||||
return payments
|
|
||||||
.Select(pay =>
|
|
||||||
{
|
|
||||||
var paymentMethodContribution = new Contribution();
|
|
||||||
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
|
|
||||||
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
|
||||||
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate;
|
|
||||||
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
|
|
||||||
return paymentMethodContribution;
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
})
|
|
||||||
.GroupBy(p => p.PaymentMethodId)
|
|
||||||
.ToDictionary(p => p.Key, p => new Contribution()
|
|
||||||
{
|
|
||||||
PaymentMethodId = p.Key,
|
|
||||||
Value = p.Select(v => v.Value).Sum(),
|
|
||||||
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
|
|
||||||
});
|
|
||||||
return new Contributions(contributions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PosHolder
|
private class PosHolder
|
||||||
{
|
{
|
||||||
|
@ -734,25 +507,25 @@ namespace BTCPayServer.Services.Apps
|
||||||
public YamlScalarNode Value { get; set; }
|
public YamlScalarNode Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
|
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, string type = null)
|
||||||
{
|
{
|
||||||
if (userId == null || appId == null)
|
if (userId == null || appId == null)
|
||||||
return null;
|
return null;
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
var app = await ctx.UserStore
|
var app = await ctx.UserStore
|
||||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return null;
|
return null;
|
||||||
if (type != null && type.Value.ToString() != app.AppType)
|
if (type != null && type != app.AppType)
|
||||||
return null;
|
return null;
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateOrCreateApp(AppData app)
|
public async Task UpdateOrCreateApp(AppData app)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
if (string.IsNullOrEmpty(app.Id))
|
if (string.IsNullOrEmpty(app.Id))
|
||||||
{
|
{
|
||||||
app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||||
|
@ -808,7 +581,35 @@ namespace BTCPayServer.Services.Apps
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||||
|
{
|
||||||
|
var app = GetAppForType(appData.AppType);
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
appData.SetSettings(null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await app.SetDefaultSettings(appData, defaultCurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> ViewLink(AppData app)
|
||||||
|
{
|
||||||
|
var appType = GetAppForType(app.AppType);
|
||||||
|
return await appType?.ViewLink(app)!;
|
||||||
|
}
|
||||||
#nullable restore
|
#nullable restore
|
||||||
|
public bool SupportsSalesStats(AppData app)
|
||||||
|
{
|
||||||
|
return GetAppForType(app.AppType).SupportsSalesStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsItemStats(AppData app)
|
||||||
|
{
|
||||||
|
return GetAppForType(app.AppType).SupportsItemStats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ItemStats
|
public class ItemStats
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Apps
|
namespace BTCPayServer.Services.Apps
|
||||||
{
|
{
|
||||||
public enum AppType
|
public interface IApp
|
||||||
{
|
{
|
||||||
[Display(Name = "Point of Sale")]
|
public string Description { get; }
|
||||||
PointOfSale,
|
public string Type { get; }
|
||||||
Crowdfund
|
public bool SupportsSalesStats { get; }
|
||||||
}
|
public bool SupportsItemStats { get; }
|
||||||
|
Task<string> ConfigureLink(AppData app);
|
||||||
public enum PosViewType
|
Task<string> ViewLink(AppData app);
|
||||||
{
|
Task SetDefaultSettings(AppData appData, string defaultCurrency);
|
||||||
[Display(Name = "Product list")]
|
Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
|
||||||
Static,
|
Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities);
|
||||||
[Display(Name = "Product list with cart")]
|
Task<object> GetInfo(AppData appData);
|
||||||
Cart,
|
|
||||||
[Display(Name = "Keypad only")]
|
|
||||||
Light,
|
|
||||||
[Display(Name = "Print display")]
|
|
||||||
Print
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RequiresRefundEmail
|
public enum RequiresRefundEmail
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Apps
|
namespace BTCPayServer.Services.Apps
|
||||||
{
|
{
|
||||||
|
|
|
@ -775,6 +775,67 @@ namespace BTCPayServer.Services.Invoices
|
||||||
? JsonConvert.DeserializeObject<T>(ZipUtils.Unzip(blob), DefaultSerializerSettings)
|
? JsonConvert.DeserializeObject<T>(ZipUtils.Unzip(blob), DefaultSerializerSettings)
|
||||||
: network.ToObject<T>(ZipUtils.Unzip(blob));
|
: network.ToObject<T>(ZipUtils.Unzip(blob));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ToJsonString<T>(T data, BTCPayNetworkBase network)
|
||||||
|
{
|
||||||
|
return network == null ? JsonConvert.SerializeObject(data, DefaultSerializerSettings) : network.ToString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public InvoiceStatistics GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
|
||||||
|
{
|
||||||
|
var contributions = invoices
|
||||||
|
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.SelectMany(p =>
|
||||||
|
{
|
||||||
|
var contribution = new InvoiceStatistics.Contribution();
|
||||||
|
contribution.PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike);
|
||||||
|
contribution.CurrencyValue = p.Price;
|
||||||
|
contribution.Value = contribution.CurrencyValue;
|
||||||
|
|
||||||
|
// For hardcap, we count newly created invoices as part of the contributions
|
||||||
|
if (!softcap && p.Status == InvoiceStatusLegacy.New)
|
||||||
|
return new[] { contribution };
|
||||||
|
|
||||||
|
// If the user get a donation via other mean, he can register an invoice manually for such amount
|
||||||
|
// then mark the invoice as complete
|
||||||
|
var payments = p.GetPayments(true);
|
||||||
|
if (payments.Count == 0 &&
|
||||||
|
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||||
|
p.Status == InvoiceStatusLegacy.Complete)
|
||||||
|
return new[] { contribution };
|
||||||
|
|
||||||
|
contribution.CurrencyValue = 0m;
|
||||||
|
contribution.Value = 0m;
|
||||||
|
|
||||||
|
// If an invoice has been marked invalid, remove the contribution
|
||||||
|
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||||
|
p.Status == InvoiceStatusLegacy.Invalid)
|
||||||
|
return new[] { contribution };
|
||||||
|
|
||||||
|
|
||||||
|
// Else, we just sum the payments
|
||||||
|
return payments
|
||||||
|
.Select(pay =>
|
||||||
|
{
|
||||||
|
var paymentMethodContribution = new InvoiceStatistics.Contribution();
|
||||||
|
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
|
||||||
|
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||||
|
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate;
|
||||||
|
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
|
||||||
|
return paymentMethodContribution;
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
})
|
||||||
|
.GroupBy(p => p.PaymentMethodId)
|
||||||
|
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution()
|
||||||
|
{
|
||||||
|
PaymentMethodId = p.Key,
|
||||||
|
Value = p.Select(v => v.Value).Sum(),
|
||||||
|
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
|
||||||
|
});
|
||||||
|
return new InvoiceStatistics(contributions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InvoiceQuery
|
public class InvoiceQuery
|
||||||
|
@ -844,4 +905,20 @@ namespace BTCPayServer.Services.Invoices
|
||||||
public bool IncludeArchived { get; set; } = true;
|
public bool IncludeArchived { get; set; } = true;
|
||||||
public bool IncludeRefunds { get; set; }
|
public bool IncludeRefunds { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class InvoiceStatistics : Dictionary<PaymentMethodId, InvoiceStatistics.Contribution>
|
||||||
|
{
|
||||||
|
public InvoiceStatistics(IEnumerable<KeyValuePair<PaymentMethodId, Contribution>> collection) : base(collection)
|
||||||
|
{
|
||||||
|
TotalCurrency = Values.Select(v => v.CurrencyValue).Sum();
|
||||||
|
}
|
||||||
|
public decimal TotalCurrency { get; }
|
||||||
|
|
||||||
|
public class Contribution
|
||||||
|
{
|
||||||
|
public PaymentMethodId PaymentMethodId { get; set; }
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
public decimal CurrencyValue { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,7 @@ namespace BTCPayServer.Services
|
||||||
|
|
||||||
[Display(Name = "Display app on website root")]
|
[Display(Name = "Display app on website root")]
|
||||||
public string RootAppId { get; set; }
|
public string RootAppId { get; set; }
|
||||||
public AppType? RootAppType { get; set; }
|
public string RootAppType { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[Display(Name = "Override the block explorers used")]
|
[Display(Name = "Override the block explorers used")]
|
||||||
public List<BlockExplorerOverrideItem> BlockExplorerLinks { get; set; } = new List<BlockExplorerOverrideItem>();
|
public List<BlockExplorerOverrideItem> BlockExplorerLinks { get; set; } = new List<BlockExplorerOverrideItem>();
|
||||||
|
@ -65,7 +64,7 @@ namespace BTCPayServer.Services
|
||||||
[Display(Name = "Domain")] [Required] [HostName] public string Domain { get; set; }
|
[Display(Name = "Domain")] [Required] [HostName] public string Domain { get; set; }
|
||||||
[Display(Name = "App")] [Required] public string AppId { get; set; }
|
[Display(Name = "App")] [Required] public string AppId { get; set; }
|
||||||
|
|
||||||
public AppType AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
@using BTCPayServer.Client
|
@using BTCPayServer.Client
|
||||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@using BTCPayServer.TagHelpers
|
|
||||||
@using BTCPayServer.Views.Apps
|
@using BTCPayServer.Views.Apps
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
@using BTCPayServer.Services.Apps
|
@using BTCPayServer.Abstractions.TagHelpers
|
||||||
|
@using BTCPayServer.Plugins.Crowdfund
|
||||||
@model BTCPayServer.Components.MainNav.StoreApp
|
@model BTCPayServer.Components.MainNav.StoreApp
|
||||||
|
|
||||||
@{ var store = Context.GetStoreData(); }
|
@{ var store = Context.GetStoreData(); }
|
||||||
|
|
||||||
@if (store != null && Model.AppType == AppType.Crowdfund)
|
@if (store != null && Model.AppType == CrowdfundApp.AppType)
|
||||||
{
|
{
|
||||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||||
<a asp-area="" asp-controller="UICrowdfund" asp-action="UpdateCrowdfund" asp-route-appId="@Model.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, Model.Id)" id="@($"StoreNav-App-{Model.Id}")">
|
<a asp-area="" asp-controller="UICrowdfund" asp-action="UpdateCrowdfund" asp-route-appId="@Model.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, Model.Id)" id="@($"StoreNav-App-{Model.Id}")">
|
||||||
<vc:icon symbol="@Model.AppType.ToString().ToLower()"/>
|
<vc:icon symbol="@Model.AppType.ToLower()"/>
|
||||||
<span>@Model.AppName</span>
|
<span>@Model.AppName</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
@using BTCPayServer.Client
|
@using BTCPayServer.Client
|
||||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@using BTCPayServer.TagHelpers
|
|
||||||
@using BTCPayServer.Views.Apps
|
@using BTCPayServer.Views.Apps
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
@using BTCPayServer.Services.Apps
|
@using BTCPayServer.Abstractions.TagHelpers
|
||||||
|
@using BTCPayServer.Plugins.PointOfSale
|
||||||
@model BTCPayServer.Components.MainNav.StoreApp
|
@model BTCPayServer.Components.MainNav.StoreApp
|
||||||
|
|
||||||
@{ var store = Context.GetStoreData(); }
|
@{ var store = Context.GetStoreData(); }
|
||||||
|
|
||||||
@if (store != null && Model.AppType == AppType.PointOfSale)
|
@if (store != null && Model.AppType == PointOfSaleApp.AppType)
|
||||||
{
|
{
|
||||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||||
<a asp-area="" asp-controller="UIPointOfSale" asp-action="UpdatePointOfSale" asp-route-appId="@Model.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, Model.Id)" id="@($"StoreNav-App-{Model.Id}")">
|
<a asp-area="" asp-controller="UIPointOfSale" asp-action="UpdatePointOfSale" asp-route-appId="@Model.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, Model.Id)" id="@($"StoreNav-App-{Model.Id}")">
|
||||||
<vc:icon symbol="@Model.AppType.ToString().ToLower()"/>
|
<vc:icon symbol="@Model.AppType.ToLower()"/>
|
||||||
<span>@Model.AppName</span>
|
<span>@Model.AppName</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
@using BTCPayServer.Services.Apps
|
|
||||||
@using BTCPayServer.Abstractions.Models
|
@using BTCPayServer.Abstractions.Models
|
||||||
@using BTCPayServer.Views.Apps
|
@using BTCPayServer.Views.Apps
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Plugins.PointOfSale
|
||||||
@using BTCPayServer.Forms
|
@using BTCPayServer.Forms
|
||||||
@using BTCPayServer.Services.Stores
|
|
||||||
@inject FormDataService FormDataService
|
@inject FormDataService FormDataService
|
||||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||||
@model BTCPayServer.Plugins.PointOfSale.Models.UpdatePointOfSaleViewModel
|
@model BTCPayServer.Plugins.PointOfSale.Models.UpdatePointOfSaleViewModel
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@using BTCPayServer.Services.Apps
|
@using BTCPayServer.Services.Apps
|
||||||
@using BTCPayServer.Abstractions.Models
|
@using BTCPayServer.Abstractions.Models
|
||||||
@model ListAppsViewModel
|
@model ListAppsViewModel
|
||||||
|
@inject AppService AppService
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(AppsNavPages.Index, "Apps");
|
ViewData.SetActivePage(AppsNavPages.Index, "Apps");
|
||||||
var storeNameSortOrder = (string)ViewData["StoreNameSortOrder"];
|
var storeNameSortOrder = (string)ViewData["StoreNameSortOrder"];
|
||||||
|
@ -89,13 +90,15 @@
|
||||||
</td>
|
</td>
|
||||||
<td>@app.AppName</td>
|
<td>@app.AppName</td>
|
||||||
<td>
|
<td>
|
||||||
@typeof(AppType).DisplayName(app.AppType)
|
@AppService.GetAvailableAppTypes()[app.AppType]
|
||||||
@if (app.AppType != AppType.Crowdfund.ToString())
|
@{
|
||||||
|
var viewStyle = @app.ViewStyle;
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(viewStyle))
|
||||||
{
|
{
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
|
@Safe.Raw(viewStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.ViewStyle
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@if (app.IsOwner)
|
@if (app.IsOwner)
|
||||||
|
@ -103,7 +106,7 @@
|
||||||
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
|
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
|
||||||
<span> - </span>
|
<span> - </span>
|
||||||
}
|
}
|
||||||
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(app.AppName)</strong> and its settings will be permanently deleted from your store <strong>@Html.Encode(app.StoreName)</strong>." data-confirm-input="DELETE">Delete</a>
|
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(app.AppName)</strong> and its settings will be permanently deleted from your store <strong>@Html.Encode(app.StoreName)</strong>." data-confirm-input="DELETE">Delete</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,9 @@
|
||||||
@using BTCPayServer.Components.StoreWalletBalance
|
@using BTCPayServer.Components.StoreWalletBalance
|
||||||
@using BTCPayServer.Components.AppSales
|
@using BTCPayServer.Components.AppSales
|
||||||
@using BTCPayServer.Components.AppTopItems
|
@using BTCPayServer.Components.AppTopItems
|
||||||
@model StoreDashboardViewModel;
|
@using BTCPayServer.Services.Apps
|
||||||
|
@inject AppService AppService
|
||||||
|
@model StoreDashboardViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
||||||
var store = ViewContext.HttpContext.GetStoreData();
|
var store = ViewContext.HttpContext.GetStoreData();
|
||||||
|
@ -118,8 +119,14 @@
|
||||||
<vc:store-recent-invoices vm="@(new StoreRecentInvoicesViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
|
<vc:store-recent-invoices vm="@(new StoreRecentInvoicesViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
|
||||||
@foreach (var app in Model.Apps)
|
@foreach (var app in Model.Apps)
|
||||||
{
|
{
|
||||||
<vc:app-sales app-id="@app.Id" app-type="@app.AppType" />
|
@if (AppService.SupportsSalesStats(app))
|
||||||
<vc:app-top-items app-id="@app.Id" app-type="@app.AppType" />
|
{
|
||||||
|
<vc:app-sales app-id="@app.Id" app-type="@app.AppType" />
|
||||||
|
}
|
||||||
|
@if (AppService.SupportsItemStats(app))
|
||||||
|
{
|
||||||
|
<vc:app-top-items app-id="@app.Id" app-type="@app.AppType" />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
<symbol id="payment-requests" viewBox="0 0 24 24" fill="none"><path d="M12 19.3845C16.0784 19.3845 19.3846 16.0783 19.3846 11.9999C19.3846 7.92144 16.0784 4.61523 12 4.61523C7.92156 4.61523 4.61536 7.92144 4.61536 11.9999C4.61536 16.0783 7.92156 19.3845 12 19.3845Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M9.53845 14.216L14.2769 9.41602" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.53845 10.707V14.2147H13.2308" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
<symbol id="payment-requests" viewBox="0 0 24 24" fill="none"><path d="M12 19.3845C16.0784 19.3845 19.3846 16.0783 19.3846 11.9999C19.3846 7.92144 16.0784 4.61523 12 4.61523C7.92156 4.61523 4.61536 7.92144 4.61536 11.9999C4.61536 16.0783 7.92156 19.3845 12 19.3845Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M9.53845 14.216L14.2769 9.41602" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.53845 10.707V14.2147H13.2308" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||||
<symbol id="payment-sent" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="22.5" stroke="currentColor" stroke-width="3"/><path d="M24 16v16m5.71-10.29L24 16l-6.29 6.29" stroke="currentColor" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
<symbol id="payment-sent" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="22.5" stroke="currentColor" stroke-width="3"/><path d="M24 16v16m5.71-10.29L24 16l-6.29 6.29" stroke="currentColor" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||||
<symbol id="payouts" viewBox="0 0 24 24" fill="none"><path d="M8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M8.30774 8.92383H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.30774 12H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.30774 15.0156H12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
<symbol id="payouts" viewBox="0 0 24 24" fill="none"><path d="M8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M8.30774 8.92383H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.30774 12H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.30774 15.0156H12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||||
|
<symbol id="plugin" viewBox="0 0 24 24" fill="none"><path d="M12.0002 10.2354L4.73633 7.38747M12.0002 10.2354L19.2642 7.38747M12.0002 10.2354V19.5M5.21166 7.01614L11.2783 4.6375C11.7412 4.45417 12.2566 4.45417 12.7196 4.6375L18.7862 7.01614C19.0023 7.1083 19.1858 7.26312 19.3131 7.46062C19.4404 7.65812 19.5055 7.88923 19.5002 8.12413V15.876C19.5058 16.1106 19.441 16.3415 19.3142 16.539C19.1874 16.7365 19.0045 16.8915 18.7888 16.984L12.7222 19.3633C12.259 19.5453 11.7441 19.5453 11.2809 19.3633L5.21433 16.984C4.9982 16.8919 4.81466 16.737 4.68739 16.5395C4.56012 16.342 4.49496 16.1109 4.50033 15.876V8.12413C4.49475 7.88953 4.55951 7.65864 4.68628 7.46117C4.81305 7.26371 4.99603 7.10871 5.21166 7.01614Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||||
<symbol id="pointofsale" viewBox="0 0 24 24" fill="none"><path d="M16.88 19.4303H7.12002C6.79748 19.4322 6.47817 19.3659 6.18309 19.2356C5.88802 19.1054 5.62385 18.9141 5.40795 18.6745C5.19206 18.4349 5.02933 18.1522 4.93046 17.8452C4.83159 17.5382 4.79882 17.2137 4.83431 16.8931L5.60002 10.1617C5.6311 9.88087 5.76509 9.62152 5.97615 9.43368C6.1872 9.24584 6.46035 9.14284 6.74288 9.14455H17.2572C17.5397 9.14284 17.8129 9.24584 18.0239 9.43368C18.235 9.62152 18.369 9.88087 18.4 10.1617L19.1429 16.8931C19.1782 17.2118 19.146 17.5343 19.0485 17.8398C18.951 18.1452 18.7903 18.4267 18.5769 18.666C18.3634 18.9053 18.1021 19.097 17.8097 19.2286C17.5174 19.3603 17.2006 19.429 16.88 19.4303Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.42859 9.14369C7.42859 7.93128 7.91022 6.76852 8.76753 5.91121C9.62484 5.0539 10.7876 4.57227 12 4.57227C13.2124 4.57227 14.3752 5.0539 15.2325 5.91121C16.0898 6.76852 16.5714 7.93128 16.5714 9.14369" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.14282 12.5723H14.8571" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
<symbol id="pointofsale" viewBox="0 0 24 24" fill="none"><path d="M16.88 19.4303H7.12002C6.79748 19.4322 6.47817 19.3659 6.18309 19.2356C5.88802 19.1054 5.62385 18.9141 5.40795 18.6745C5.19206 18.4349 5.02933 18.1522 4.93046 17.8452C4.83159 17.5382 4.79882 17.2137 4.83431 16.8931L5.60002 10.1617C5.6311 9.88087 5.76509 9.62152 5.97615 9.43368C6.1872 9.24584 6.46035 9.14284 6.74288 9.14455H17.2572C17.5397 9.14284 17.8129 9.24584 18.0239 9.43368C18.235 9.62152 18.369 9.88087 18.4 10.1617L19.1429 16.8931C19.1782 17.2118 19.146 17.5343 19.0485 17.8398C18.951 18.1452 18.7903 18.4267 18.5769 18.666C18.3634 18.9053 18.1021 19.097 17.8097 19.2286C17.5174 19.3603 17.2006 19.429 16.88 19.4303Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.42859 9.14369C7.42859 7.93128 7.91022 6.76852 8.76753 5.91121C9.62484 5.0539 10.7876 4.57227 12 4.57227C13.2124 4.57227 14.3752 5.0539 15.2325 5.91121C16.0898 6.76852 16.5714 7.93128 16.5714 9.14369" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.14282 12.5723H14.8571" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||||
<symbol id="pos-cart" viewBox="0 0 24 24" fill="none"><path d="M16.88 19.426H7.12a2.286 2.286 0 0 1-2.286-2.537l.766-6.731A1.143 1.143 0 0 1 6.743 9.14h10.514a1.143 1.143 0 0 1 1.143 1.017l.743 6.731a2.286 2.286 0 0 1-2.263 2.537Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.43 9.142a4.571 4.571 0 1 1 9.143 0M9.14 12.57h5.715" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
<symbol id="pos-cart" viewBox="0 0 24 24" fill="none"><path d="M16.88 19.426H7.12a2.286 2.286 0 0 1-2.286-2.537l.766-6.731A1.143 1.143 0 0 1 6.743 9.14h10.514a1.143 1.143 0 0 1 1.143 1.017l.743 6.731a2.286 2.286 0 0 1-2.263 2.537Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.43 9.142a4.571 4.571 0 1 1 9.143 0M9.14 12.57h5.715" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||||
<symbol id="pos-light" viewBox="0 0 24 24" fill="none"><path d="M8 4h8c2.2 0 4 1.8 4 4v8c0 2.2-1.8 4-4 4H8c-2.2 0-4-1.8-4-4V8c0-2.2 1.8-4 4-4Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M8 13h8M8 16.25h8" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><rect x="7" y="7" width="10" height="3.5" rx="1" fill="currentColor"/></symbol>
|
<symbol id="pos-light" viewBox="0 0 24 24" fill="none"><path d="M8 4h8c2.2 0 4 1.8 4 4v8c0 2.2-1.8 4-4 4H8c-2.2 0-4-1.8-4-4V8c0-2.2 1.8-4 4-4Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M8 13h8M8 16.25h8" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><rect x="7" y="7" width="10" height="3.5" rx="1" fill="currentColor"/></symbol>
|
||||||
|
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Loading…
Add table
Reference in a new issue