diff --git a/BTCPayServer.Client/Models/AppItem.cs b/BTCPayServer.Client/Models/AppItem.cs new file mode 100644 index 000000000..c749df304 --- /dev/null +++ b/BTCPayServer.Client/Models/AppItem.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using BTCPayServer.JsonConverters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Client.Models; + +public enum AppItemPriceType +{ + Fixed, + Topup, + Minimum +} + +public class AppItem +{ + public string Id { get; set; } + public string Title { get; set; } + public bool Disabled { get; set; } + public string Description { get; set; } + public string[] Categories { get; set; } + public string Image { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public AppItemPriceType PriceType { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal? Price { get; set; } + public string BuyButtonText { get; set; } + public int? Inventory { get; set; } + + [JsonExtensionData] + public Dictionary AdditionalData { get; set; } +} diff --git a/BTCPayServer.Client/Models/CrowdfundAppData.cs b/BTCPayServer.Client/Models/CrowdfundAppData.cs index 792453482..65da5416a 100644 --- a/BTCPayServer.Client/Models/CrowdfundAppData.cs +++ b/BTCPayServer.Client/Models/CrowdfundAppData.cs @@ -37,7 +37,7 @@ public abstract class CrowdfundBaseData : AppBaseData public class CrowdfundAppData : CrowdfundBaseData { - public object? Perks { get; set; } + public AppItem[]? Perks { get; set; } } public class CrowdfundAppRequest : CrowdfundBaseData, IAppRequest diff --git a/BTCPayServer.Client/Models/PointOfSaleAppData.cs b/BTCPayServer.Client/Models/PointOfSaleAppData.cs index 70b805831..035e7a701 100644 --- a/BTCPayServer.Client/Models/PointOfSaleAppData.cs +++ b/BTCPayServer.Client/Models/PointOfSaleAppData.cs @@ -1,8 +1,6 @@ #nullable enable -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Client.Models; @@ -31,7 +29,7 @@ public abstract class PointOfSaleBaseData : AppBaseData public class PointOfSaleAppData : PointOfSaleBaseData { - public object? Items { get; set; } + public AppItem[]? Items { get; set; } } public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index 5ec8f80e9..0938bcc0e 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; -using BTCPayServer.Events; -using BTCPayServer.HostedServices; using BTCPayServer.Hosting; using BTCPayServer.Lightning; using BTCPayServer.Models.AppViewModels; @@ -25,7 +24,7 @@ using Newtonsoft.Json.Linq; using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; -using Xunit.Sdk; +using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType; using WalletSettingsViewModel = BTCPayServer.Models.StoreViewModels.WalletSettingsViewModel; namespace BTCPayServer.Tests @@ -759,39 +758,6 @@ noninventoryitem: AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory); }, 10000); - //test payment methods option - vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync(); - vmpos.Title = "hello"; - vmpos.Currency = "BTC"; - vmpos.Template = @" -btconly: - price: 1.0 - title: good apple - payment_methods: - - BTC -normal: - price: 1.0"; - vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template)); - Assert.IsType(pos.UpdatePointOfSale(app.Id, vmpos).Result); - Assert.IsType(publicApps - .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result); - Assert.IsType(publicApps - .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result); - invoices = user.BitPay.GetInvoices(); - var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); - var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); - Assert.Single(btcOnlyInvoice.CryptoInfo); - Assert.Equal("BTC", - btcOnlyInvoice.CryptoInfo.First().CryptoCode); - Assert.Equal("BTC-CHAIN", - btcOnlyInvoice.CryptoInfo.First().PaymentType); - - Assert.Equal(2, normalInvoice.CryptoInfo.Length); - Assert.Contains( - normalInvoice.CryptoInfo, - s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains( - s.CryptoCode)); - //test topup option vmpos.Template = @" a: @@ -821,13 +787,13 @@ g: vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync(); Assert.DoesNotContain("custom", vmpos.Template); var items = AppService.Parse(vmpos.Template); - Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed); - Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed); - Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum); - Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed); - Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum); - Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup); - Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup); + Assert.Contains(items, item => item.Id == "a" && item.PriceType == AppItemPriceType.Fixed); + Assert.Contains(items, item => item.Id == "b" && item.PriceType == AppItemPriceType.Fixed); + Assert.Contains(items, item => item.Id == "c" && item.PriceType == AppItemPriceType.Minimum); + Assert.Contains(items, item => item.Id == "d" && item.PriceType == AppItemPriceType.Fixed); + Assert.Contains(items, item => item.Id == "e" && item.PriceType == AppItemPriceType.Minimum); + Assert.Contains(items, item => item.Id == "f" && item.PriceType == AppItemPriceType.Topup); + Assert.Contains(items, item => item.Id == "g" && item.PriceType == AppItemPriceType.Topup); Assert.IsType(publicApps .ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index b69229ff0..f995ae92a 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -745,9 +745,9 @@ namespace BTCPayServer.Tests await user.RegisterDerivationSchemeAsync("BTC"); var client = await user.CreateClient(); - var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed }; - var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed }; - var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed }; + var item1 = new AppItem { Id = "item1", Title = "Item 1", Price = 1, PriceType = AppItemPriceType.Fixed }; + var item2 = new AppItem { Id = "item2", Title = "Item 2", Price = 2, PriceType = AppItemPriceType.Fixed }; + var item3 = new AppItem { Id = "item3", Title = "Item 3", Price = 3, PriceType = AppItemPriceType.Fixed }; var posItems = AppService.SerializeTemplate([item1, item2, item3]); var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, }); var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" }); diff --git a/BTCPayServer.Tests/POSTests.cs b/BTCPayServer.Tests/POSTests.cs index b4a1b447c..732dc6380 100644 --- a/BTCPayServer.Tests/POSTests.cs +++ b/BTCPayServer.Tests/POSTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using BTCPayServer.Client; +using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Hosting; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Mvc; using Xunit; using Xunit.Abstractions; using static BTCPayServer.Tests.UnitTest1; +using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType; namespace BTCPayServer.Tests { @@ -76,9 +78,8 @@ fruit tea: Assert.Null( parsedDefault[0].BuyButtonText); Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image); Assert.Equal( 1 ,parsedDefault[0].Price); - Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType); + Assert.Equal( AppItemPriceType.Fixed ,parsedDefault[0].PriceType); Assert.Null( parsedDefault[0].AdditionalData); - Assert.Null( parsedDefault[0].PaymentMethods); Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title); @@ -87,9 +88,8 @@ fruit tea: Assert.Null( parsedDefault[4].BuyButtonText); Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image); Assert.Equal( 1.8m ,parsedDefault[4].Price); - Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType); + Assert.Equal( AppItemPriceType.Minimum ,parsedDefault[4].PriceType); Assert.Null( parsedDefault[4].AdditionalData); - Assert.Null( parsedDefault[4].PaymentMethods); } [Fact] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index 1b33362d1..670cd0e86 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -355,6 +355,7 @@ namespace BTCPayServer.Controllers.Greenfield { var settings = appData.GetSettings(); Enum.TryParse(settings.DefaultView.ToString(), true, out var defaultView); + var items = AppService.Parse(settings.Template); return new PointOfSaleAppData { @@ -382,16 +383,7 @@ namespace BTCPayServer.Controllers.Greenfield RedirectUrl = settings.RedirectUrl, Description = settings.Description, RedirectAutomatically = settings.RedirectAutomatically, - Items = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject( - AppService.Parse(settings.Template), - new JsonSerializerSettings - { - ContractResolver = - new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() - } - ) - ) + Items = items }; } @@ -420,6 +412,7 @@ namespace BTCPayServer.Controllers.Greenfield { var settings = appData.GetSettings(); Enum.TryParse(settings.ResetEvery.ToString(), true, out var resetEvery); + var perks = AppService.Parse(settings.PerksTemplate); return new CrowdfundAppData { @@ -451,15 +444,7 @@ namespace BTCPayServer.Controllers.Greenfield SortPerksByPopularity = settings.SortPerksByPopularity, Sounds = settings.Sounds, AnimationColors = settings.AnimationColors, - Perks = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject( - AppService.Parse(settings.PerksTemplate), - new JsonSerializerSettings - { - ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() - } - ) - ) + Perks = perks }; } diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 2cd7f9fcc..1f15ef5f2 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -26,7 +26,6 @@ using BTCPayServer.Payouts; using BTCPayServer.Plugins; using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.PointOfSale; -using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; @@ -290,7 +289,7 @@ namespace BTCPayServer return NotFound(); } - ViewPointOfSaleViewModel.Item[] items; + AppItem[] items; string currencyCode; PointOfSaleSettings posS = null; switch (app.AppType) @@ -310,7 +309,7 @@ namespace BTCPayServer return NotFound(); } - ViewPointOfSaleViewModel.Item item = null; + AppItem item = null; if (!string.IsNullOrEmpty(itemCode)) { var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _); @@ -321,13 +320,8 @@ namespace BTCPayServer item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) || item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase)); - if (item is null || - item.Inventory <= 0 || - (item.PaymentMethods?.Any() is true && - item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false)) - { + if (item is null || item.Inventory <= 0) return NotFound(); - } } else if (app.AppType == PointOfSaleAppType.AppType && posS?.ShowCustomAmount is not true) { @@ -336,7 +330,7 @@ namespace BTCPayServer var createInvoice = new CreateInvoiceRequest { - Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price, + Amount = item?.PriceType == AppItemPriceType.Topup ? null : item?.Price, Currency = currencyCode, Checkout = new InvoiceDataBase.CheckoutOptions { @@ -350,7 +344,7 @@ namespace BTCPayServer AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) } }; - var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed; + var allowOverpay = item?.PriceType is not AppItemPriceType.Fixed; var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() }; if (item != null) { diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index 336010511..a45b10b0d 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Fido2; @@ -12,11 +13,9 @@ using BTCPayServer.Fido2.Models; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; -using BTCPayServer.PayoutProcessors; using BTCPayServer.Payouts; using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.PointOfSale; -using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; @@ -398,9 +397,10 @@ namespace BTCPayServer.Hosting await ctx.SaveChangesAsync(); } - public static ViewPointOfSaleViewModel.Item[] ParsePOSYML(string yaml) + + public static AppItem[] ParsePOSYML(string yaml) { - var items = new List(); + var items = new List(); var stream = new YamlStream(); if (string.IsNullOrEmpty(yaml)) return items.ToArray(); @@ -417,11 +417,11 @@ namespace BTCPayServer.Hosting continue; } - var currentItem = new ViewPointOfSaleViewModel.Item + var currentItem = new AppItem { Id = trimmedKey, Title = trimmedKey, - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed + PriceType = AppItemPriceType.Fixed }; var itemSpecs = (YamlMappingNode)posItem.Value; foreach (var spec in itemSpecs) @@ -446,12 +446,6 @@ namespace BTCPayServer.Hosting case "image": currentItem.Image = scalarValue?.Value; break; - case "payment_methods" when spec.Value is YamlSequenceNode pmSequenceNode: - - currentItem.PaymentMethods = pmSequenceNode.Children - .Select(node => (node as YamlScalarNode)?.Value?.Trim()) - .Where(node => !string.IsNullOrEmpty(node)).ToArray(); - break; case "price_type": case "custom": if (bool.TryParse(scalarValue?.Value, out var customBoolValue)) @@ -459,15 +453,15 @@ namespace BTCPayServer.Hosting if (customBoolValue) { currentItem.PriceType = currentItem.Price is null or 0 - ? ViewPointOfSaleViewModel.ItemPriceType.Topup - : ViewPointOfSaleViewModel.ItemPriceType.Minimum; + ? AppItemPriceType.Topup + : AppItemPriceType.Minimum; } else { - currentItem.PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed; + currentItem.PriceType = AppItemPriceType.Fixed; } } - else if (Enum.TryParse(scalarValue?.Value, true, + else if (Enum.TryParse(scalarValue?.Value, true, out var customPriceType)) { currentItem.PriceType = customPriceType; diff --git a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs index a3b760f29..c7f9618d6 100644 --- a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs +++ b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs @@ -17,7 +17,6 @@ using BTCPayServer.Forms; using BTCPayServer.Forms.Models; using BTCPayServer.Models; using BTCPayServer.Plugins.Crowdfund.Models; -using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; @@ -149,36 +148,28 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers decimal? price = request.Amount; var title = settings.Title; Dictionary paymentMethods = null; - ViewPointOfSaleViewModel.Item choice = null; if (!string.IsNullOrEmpty(request.ChoiceKey)) { var choices = AppService.Parse(settings.PerksTemplate, false); - choice = choices?.FirstOrDefault(c => c.Id == request.ChoiceKey); + AppItem choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey); if (choice == null) return NotFound("Incorrect option provided"); title = choice.Title; - if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) + if (choice.PriceType == AppItemPriceType.Topup) { price = null; } else { - price = choice.Price.Value; + if (choice.Price.HasValue) + price = choice.Price.Value; if (request.Amount > price) price = request.Amount; } - if (choice.Inventory.HasValue) + if (choice.Inventory is <= 0) { - if (choice.Inventory <= 0) - { - return NotFound("Option was out of stock"); - } - } - if (choice?.PaymentMethods?.Any() is true) - { - paymentMethods = choice?.PaymentMethods.ToDictionary(s => s, - s => new InvoiceSupportedTransactionCurrency() { Enabled = true }); + return NotFound("Option was out of stock"); } } else @@ -231,8 +222,6 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers } } - - if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price > (info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount)))) { diff --git a/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs b/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs index b2af61572..6c4871538 100644 --- a/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs +++ b/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using BTCPayServer.Client.Models; using BTCPayServer.Models; -using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services.Rates; namespace BTCPayServer.Plugins.Crowdfund.Models @@ -26,7 +26,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Models public CrowdfundInfo Info { get; set; } public string Tagline { get; set; } public StoreBrandingViewModel StoreBranding { get; set; } - public ViewPointOfSaleViewModel.Item[] Perks { get; set; } + public AppItem[] Perks { get; set; } public bool SimpleDisplay { get; set; } public bool DisqusEnabled { get; set; } public bool SoundsEnabled { get; set; } diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs index 96a901845..2e02a6d22 100644 --- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs +++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs @@ -176,9 +176,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers string title; decimal? price; Dictionary paymentMethods = null; - ViewPointOfSaleViewModel.Item choice = null; + AppItem choice = null; List cartItems = null; - ViewPointOfSaleViewModel.Item[] choices = null; + AppItem[] choices = null; if (!string.IsNullOrEmpty(choiceKey)) { choices = AppService.Parse(settings.Template, false); @@ -186,7 +186,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers if (choice == null) return NotFound(); title = choice.Title; - if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) + if (choice.PriceType == AppItemPriceType.Topup) { price = null; } @@ -201,12 +201,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers { return RedirectToAction(nameof(ViewPointOfSale), new { appId }); } - - if (choice?.PaymentMethods?.Any() is true) - { - paymentMethods = choice?.PaymentMethods.ToDictionary(s => s, - s => new InvoiceSupportedTransactionCurrency() { Enabled = true }); - } } else { @@ -239,7 +233,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers } } - var expectedCartItemPrice = itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup + var expectedCartItemPrice = itemChoice.PriceType != AppItemPriceType.Topup ? itemChoice.Price ?? 0 : 0; @@ -373,7 +367,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers var singlePrice = _displayFormatter.Currency(cartItem.Price, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol); var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol); var ident = selectedChoice.Title ?? selectedChoice.Id; - var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})"; + var key = selectedChoice.PriceType == AppItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})"; cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}"); } diff --git a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs index 31a76dd11..0e4104a15 100644 --- a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs @@ -2,52 +2,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using BTCPayServer.JsonConverters; +using BTCPayServer.Client.Models; using BTCPayServer.Models; -using BTCPayServer.Services.Apps; using Microsoft.AspNetCore.Mvc.Rendering; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Plugins.PointOfSale.Models { public class ViewPointOfSaleViewModel { - public enum ItemPriceType - { - Topup, - Minimum, - Fixed - } - - public class Item - { - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - public string Id { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string[] Categories { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string Image { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public ItemPriceType PriceType { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal? Price { get; set; } - public string Title { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string BuyButtonText { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public int? Inventory { get; set; } = null; - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string[] PaymentMethods { get; set; } - public bool Disabled { get; set; } = false; - - [JsonExtensionData] public Dictionary AdditionalData { get; set; } - } - public class CurrencyInfoData { public bool Prefixed { get; set; } @@ -70,8 +32,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models public bool EnableTips { get; set; } public string Step { get; set; } public string Title { get; set; } - Item[] _Items; - public Item[] Items + AppItem[] _Items; + public AppItem[] Items { get { diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 1ffa84a58..755c3170f 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -10,7 +10,6 @@ using BTCPayServer.Data; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.PointOfSale; -using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; @@ -95,7 +94,7 @@ namespace BTCPayServer.Services.Apps return await salesType.GetItemStats(appData, paidInvoices); } - public static Task GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items, + public static Task GetSalesStatswithPOSItems(AppItem[] items, InvoiceEntity[] paidInvoices, int numberOfDays) { var series = paidInvoices @@ -145,7 +144,7 @@ namespace BTCPayServer.Services.Apps public DateTime Date { get; set; } } - public static Func, InvoiceEntity, List> AggregateInvoiceEntitiesForStats(ViewPointOfSaleViewModel.Item[] items) + public static Func, InvoiceEntity, List> AggregateInvoiceEntitiesForStats(AppItem[] items) { return (res, e) => { @@ -344,14 +343,14 @@ namespace BTCPayServer.Services.Apps return _storeRepository.FindStore(app.StoreDataId); } - public static string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items) + public static string SerializeTemplate(AppItem[] items) { return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer); } - public static ViewPointOfSaleViewModel.Item[] Parse(string template, bool includeDisabled = true, bool throws = false) + public static AppItem[] Parse(string template, bool includeDisabled = true, bool throws = false) { if (string.IsNullOrWhiteSpace(template)) return []; - var allItems = JsonConvert.DeserializeObject(template, _defaultSerializer)!; + var allItems = JsonConvert.DeserializeObject(template, _defaultSerializer)!; // ensure all items have an id, which is also unique var itemsWithoutId = allItems.Where(i => string.IsNullOrEmpty(i.Id)).ToList(); if (itemsWithoutId.Any() && throws) throw new ArgumentException($"Missing ID for item \"{itemsWithoutId.First().Title}\"."); diff --git a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs index 44f424a9d..d2e0c2bae 100644 --- a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs +++ b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs @@ -1,4 +1,4 @@ -using BTCPayServer.Plugins.PointOfSale.Models; +using BTCPayServer.Client.Models; using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType; namespace BTCPayServer.Services.Apps @@ -8,71 +8,70 @@ namespace BTCPayServer.Services.Apps public PointOfSaleSettings() { Title = "Tea shop"; - Template = AppService.SerializeTemplate(new ViewPointOfSaleViewModel.Item[] - { - new() + Template = AppService.SerializeTemplate([ + new AppItem { Id = "green-tea", Title = "Green Tea", Description = "Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.", Image = "~/img/pos-sample/green-tea.jpg", - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed, + PriceType = AppItemPriceType.Fixed, Price = 1 }, - new() + new AppItem { Id = "black-tea", Title = "Black Tea", Description = "Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.", Image = "~/img/pos-sample/black-tea.jpg", - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed, + PriceType = AppItemPriceType.Fixed, Price = 1 }, - new() + new AppItem { Id = "rooibos", Title = "Rooibos (limited)", Description = "Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.", Image = "~/img/pos-sample/rooibos.jpg", - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed, + PriceType = AppItemPriceType.Fixed, Price = 1.2m, Inventory = 5, }, - new() + new AppItem { Id = "pu-erh", Title = "Pu Erh (free)", Description = "This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.", Image = "~/img/pos-sample/pu-erh.jpg", - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed, + PriceType = AppItemPriceType.Fixed, Price = 0 }, - new() + new AppItem { Id = "herbal-tea", Title = "Herbal Tea (minimum)", Description = "Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!", Image = "~/img/pos-sample/herbal-tea.jpg", - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Minimum, + PriceType = AppItemPriceType.Minimum, Price = 1.8m, Disabled = false }, - new() + new AppItem { Id = "fruit-tea", Title = "Fruit Tea (any amount)", Description = "The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!", Image = "~/img/pos-sample/fruit-tea.jpg", - PriceType = ViewPointOfSaleViewModel.ItemPriceType.Topup, + PriceType = AppItemPriceType.Topup, Disabled = false } - }); + ]); DefaultView = PosViewType.Static; ShowCustomAmount = false; ShowDiscount = false; diff --git a/BTCPayServer/Services/Invoices/PosAppData.cs b/BTCPayServer/Services/Invoices/PosAppData.cs index 685b20ce4..51002264e 100644 --- a/BTCPayServer/Services/Invoices/PosAppData.cs +++ b/BTCPayServer/Services/Invoices/PosAppData.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using BTCPayServer.Plugins.PointOfSale.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml b/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml index 6e5aa22b3..fbccf0515 100644 --- a/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml +++ b/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml @@ -1,4 +1,5 @@ -@using BTCPayServer.Plugins.PointOfSale.Models +@using BTCPayServer.Client.Models +@using Microsoft.AspNetCore.Mvc.TagHelpers @model BTCPayServer.Plugins.Crowdfund.Models.ContributeToCrowdfund @{ var vm = Model.ViewCrowdfundViewModel; } @@ -32,16 +33,16 @@ @item.Price.Value @vm.TargetCurrency - if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum) + if (item.PriceType == AppItemPriceType.Minimum) { @Safe.Raw(StringLocalizer["or more"]) } } - else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ) + else if (item.PriceType == AppItemPriceType.Topup) { @Safe.Raw(StringLocalizer["Any amount"]) } - else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed) + else if (item.PriceType == AppItemPriceType.Fixed) { @Safe.Raw(StringLocalizer["Free"]) } diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml index c90e68caa..1d05b9720 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml @@ -1,9 +1,9 @@ -@using BTCPayServer.Plugins.PointOfSale.Models @using BTCPayServer.Services @using Microsoft.AspNetCore.Mvc.TagHelpers @using Newtonsoft.Json.Linq @using BTCPayServer.Client @using BTCPayServer.Abstractions.TagHelpers +@using BTCPayServer.Client.Models @inject DisplayFormatter DisplayFormatter @inject BTCPayServer.Security.ContentSecurityPolicies Csp @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @@ -21,12 +21,12 @@ } @functions { - private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item) + private string GetItemPriceFormatted(AppItem item) { - if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) return "any amount"; + if (item.PriceType == AppItemPriceType.Topup) return "any amount"; if (item.Price == 0) return "free"; var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol); - return item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum ? $"{formatted} minimum" : formatted; + return item.PriceType == AppItemPriceType.Minimum ? $"{formatted} minimum" : formatted; } } @@ -73,7 +73,7 @@ var formatted = GetItemPriceFormatted(item); var inStock = item.Inventory is null or > 0; var buttonText = string.IsNullOrEmpty(item.BuyButtonText) - ? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText + ? item.PriceType == AppItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted); var categories = new JArray(item.Categories ?? new object[] { }); @@ -86,7 +86,7 @@
@Safe.Raw(item.Title)
- @if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup || item.Price == 0) + @if (item.PriceType == AppItemPriceType.Topup || item.Price == 0) { @Safe.Raw(char.ToUpper(formatted[0]) + formatted[1..]) } @@ -116,7 +116,7 @@ @if (inStock) {