From 9b5c8a8254e4a7049c5b23bf3406e8f27f8bcc28 Mon Sep 17 00:00:00 2001 From: d11n Date: Thu, 14 Mar 2024 11:11:54 +0100 Subject: [PATCH] POS: Add item list to keypad (#5814) * Add admin option to show item list for keypad view * Refactor common POS Vue mixin * Add item list to POS keypad * Add recent transactions to cart * Keypad: Pass tip and discount as cart does * Keypad and cart tests * Improve offcanvas button --------- Co-authored-by: Andrew Camilleri --- .../Models/CreateAppRequest.cs | 1 + .../Models/PointOfSaleAppData.cs | 1 + BTCPayServer.Tests/SeleniumTester.cs | 21 +- BTCPayServer.Tests/SeleniumTests.cs | 200 ++++++++++--- BTCPayServer.Tests/UnitTest1.cs | 8 +- .../GreenField/GreenfieldAppsController.cs | 2 + .../Controllers/UIPointOfSaleController.cs | 17 +- .../Models/UpdatePointOfSaleViewModel.cs | 2 + .../Models/ViewPointOfSaleViewModel.cs | 1 + .../Services/Apps/PointOfSaleSettings.cs | 1 + .../Shared/PointOfSale/Public/Cart.cshtml | 12 +- .../Public/RecentTransactions.cshtml | 36 +++ .../Shared/PointOfSale/Public/VueLight.cshtml | 122 ++++++-- .../PointOfSale/UpdatePointOfSale.cshtml | 8 + BTCPayServer/wwwroot/pos/admin.js | 10 +- BTCPayServer/wwwroot/pos/cart.css | 46 +-- BTCPayServer/wwwroot/pos/cart.js | 202 +------------ BTCPayServer/wwwroot/pos/common.css | 35 +++ BTCPayServer/wwwroot/pos/common.js | 280 ++++++++++++++++-- BTCPayServer/wwwroot/pos/keypad.css | 46 +-- BTCPayServer/wwwroot/pos/keypad.js | 48 +-- .../swagger/v1/swagger.template.apps.json | 6 + 22 files changed, 712 insertions(+), 393 deletions(-) create mode 100644 BTCPayServer/Views/Shared/PointOfSale/Public/RecentTransactions.cshtml diff --git a/BTCPayServer.Client/Models/CreateAppRequest.cs b/BTCPayServer.Client/Models/CreateAppRequest.cs index 7a6aa1217..d665be80f 100644 --- a/BTCPayServer.Client/Models/CreateAppRequest.cs +++ b/BTCPayServer.Client/Models/CreateAppRequest.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Client.Models public string Template { get; set; } = null; [JsonConverter(typeof(StringEnumConverter))] public PosViewType DefaultView { get; set; } + public bool ShowItems { get; set; } = false; public bool ShowCustomAmount { get; set; } = false; public bool ShowDiscount { get; set; } = false; public bool ShowSearch { get; set; } = true; diff --git a/BTCPayServer.Client/Models/PointOfSaleAppData.cs b/BTCPayServer.Client/Models/PointOfSaleAppData.cs index f2eba42fe..232811e33 100644 --- a/BTCPayServer.Client/Models/PointOfSaleAppData.cs +++ b/BTCPayServer.Client/Models/PointOfSaleAppData.cs @@ -19,6 +19,7 @@ namespace BTCPayServer.Client.Models { public string Title { get; set; } public string DefaultView { get; set; } + public bool ShowItems { get; set; } public bool ShowCustomAmount { get; set; } public bool ShowDiscount { get; set; } public bool ShowSearch { get; set; } diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 4b9df09ba..479e1404a 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -90,7 +90,6 @@ namespace BTCPayServer.Tests public void PayInvoice(bool mine = false, decimal? amount = null) { - if (amount is not null) { Driver.FindElement(By.Id("test-payment-amount")).Clear(); @@ -98,12 +97,12 @@ namespace BTCPayServer.Tests } Driver.WaitUntilAvailable(By.Id("FakePayment")); Driver.FindElement(By.Id("FakePayment")).Click(); + TestUtils.Eventually(() => + { + Driver.WaitForElement(By.Id("CheatSuccessMessage")); + }); if (mine) { - TestUtils.Eventually(() => - { - Driver.WaitForElement(By.Id("CheatSuccessMessage")); - }); MineBlockOnInvoiceCheckout(); } } @@ -646,6 +645,18 @@ retry: } } + public void AddUserToStore(string storeId, string email, string role) + { + if (Driver.FindElements(By.Id("AddUser")).Count == 0) + { + GoToStore(storeId, StoreNavPages.Users); + } + Driver.FindElement(By.Id("Email")).SendKeys(email); + new SelectElement(Driver.FindElement(By.Id("Role"))).SelectByValue(role); + Driver.FindElement(By.Id("AddUser")).Click(); + Assert.Contains("User added successfully", FindAlertMessage().Text); + } + public void AssertPageAccess(bool shouldHaveAccess, string url) { GoToUrl(url); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 1206617da..edcf786a0 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; @@ -14,10 +13,8 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Client.Models; -using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Lightning; -using BTCPayServer.Models.InvoicingModels; using BTCPayServer.NTag424; using BTCPayServer.Payments; using BTCPayServer.Services; @@ -27,7 +24,6 @@ using BTCPayServer.Views.Manage; using BTCPayServer.Views.Server; using BTCPayServer.Views.Stores; using BTCPayServer.Views.Wallets; -using Dapper; using ExchangeSharp; using LNURL; using Microsoft.AspNetCore.Identity; @@ -42,7 +38,6 @@ using OpenQA.Selenium.Support.Extensions; using OpenQA.Selenium.Support.UI; using Xunit; using Xunit.Abstractions; -using Xunit.Sdk; namespace BTCPayServer.Tests { @@ -2504,7 +2499,6 @@ namespace BTCPayServer.Tests using var s = CreateSeleniumTester(); s.Server.ActivateLightning(); await s.StartAsync(); - await s.Server.EnsureChannelsSetup(); s.RegisterNewUser(true); s.CreateNewStore(); @@ -2512,12 +2506,7 @@ namespace BTCPayServer.Tests s.AddLightningNode(LightningConnectionType.CLightning, false); s.GoToLightningSettings(); s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true); - s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click(); - - var appName = $"PoS-{Guid.NewGuid().ToString()[..21]}"; - s.Driver.FindElement(By.Id("AppName")).SendKeys(appName); - s.Driver.FindElement(By.Id("Create")).Click(); - TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text)); + s.CreateApp("PointOfSale"); s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Print']")).Click(); s.Driver.FindElement(By.Id("SaveSettings")).Click(); Assert.Contains("App updated", s.FindAlertMessage().Text); @@ -2536,13 +2525,10 @@ namespace BTCPayServer.Tests [Fact] [Trait("Selenium", "Selenium")] - [Trait("Lightning", "Lightning")] public async Task CanUsePOSKeypad() { using var s = CreateSeleniumTester(); - s.Server.ActivateLightning(); await s.StartAsync(); - await s.Server.EnsureChannelsSetup(); // Create users var user = s.RegisterNewUser(); @@ -2553,25 +2539,16 @@ namespace BTCPayServer.Tests s.RegisterNewUser(true); // Setup store and associate user - s.CreateNewStore(); + (_, string storeId) = s.CreateNewStore(); s.GoToStore(); - s.AddLightningNode(LightningConnectionType.CLightning, false); - s.GoToStore(StoreNavPages.Users); - s.Driver.FindElement(By.Id("Email")).Clear(); - s.Driver.FindElement(By.Id("Email")).SendKeys(user); - new SelectElement(s.Driver.FindElement(By.Id("Role"))).SelectByValue("Guest"); - s.Driver.FindElement(By.Id("AddUser")).Click(); - Assert.Contains("User added successfully", s.FindAlertMessage().Text); + s.AddDerivationScheme(); + s.AddUserToStore(storeId, user, "Guest"); // Setup POS - var appName = $"PoS-{Guid.NewGuid().ToString()[..21]}"; - s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click(); - s.Driver.FindElement(By.Id("AppName")).SendKeys(appName); - s.Driver.FindElement(By.Id("Create")).Click(); - TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text)); + s.CreateApp("PointOfSale"); + var editUrl = s.Driver.Url; s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click(); s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR"); - Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected); s.Driver.FindElement(By.Id("EnableTips")).Click(); Assert.True(s.Driver.FindElement(By.Id("EnableTips")).Selected); @@ -2579,9 +2556,12 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear(); s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21"); Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected); + Assert.False(s.Driver.FindElement(By.Id("ShowItems")).Selected); s.Driver.FindElement(By.Id("ShowDiscount")).Click(); s.Driver.FindElement(By.Id("SaveSettings")).Click(); Assert.Contains("App updated", s.FindAlertMessage().Text); + + // View s.Driver.FindElement(By.Id("ViewApp")).Click(); var windows = s.Driver.WindowHandles; Assert.Equal(2, windows.Count); @@ -2591,6 +2571,7 @@ namespace BTCPayServer.Tests // basic checks var keypadUrl = s.Driver.Url; s.Driver.FindElement(By.Id("RecentTransactionsToggle")); + s.Driver.ElementDoesNotExist(By.Id("ItemsListToggle")); Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text); Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text); Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text); @@ -2636,6 +2617,86 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("DetailsToggle")).Click(); s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat")); Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + s.PayInvoice(true); + TestUtils.Eventually(() => + { + s.MineBlockOnInvoiceCheckout(); + Assert.True(s.Driver.WaitForElement(By.Id("settled")).Displayed); + }); + + // Receipt + s.Driver.WaitForElement(By.Id("ReceiptLink")).Click(); + var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table")); + var items = additionalData.FindElements(By.CssSelector("tbody tr")); + var sums = additionalData.FindElements(By.CssSelector("tfoot tr")); + Assert.Equal(2, items.Count); + Assert.Equal(4, sums.Count); + Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector("th")).Text); + Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Discount", sums[1].FindElement(By.CssSelector("th")).Text); + Assert.Contains("10% = 123,46 €", sums[1].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Tip", sums[2].FindElement(By.CssSelector("th")).Text); + Assert.Contains("10% = 111,11 €", sums[2].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Total", sums[3].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector("td")).Text); + + // Once more with items + s.GoToUrl(editUrl); + s.Driver.FindElement(By.Id("ShowItems")).Click(); + s.Driver.FindElement(By.Id("SaveSettings")).Click(); + Assert.Contains("App updated", s.FindAlertMessage().Text); + + s.GoToUrl(keypadUrl); + s.Driver.WaitForElement(By.ClassName("keypad")); + s.Driver.FindElement(By.Id("ItemsListToggle")).Click(); + Thread.Sleep(250); + Assert.True(s.Driver.WaitForElement(By.Id("PosItems")).Displayed); + s.Driver.FindElement(By.CssSelector("#PosItems .posItem--displayed:nth-child(1) .btn-plus")).Click(); + s.Driver.FindElement(By.CssSelector("#PosItems .posItem--displayed:nth-child(1) .btn-plus")).Click(); + s.Driver.FindElement(By.CssSelector("#PosItems .posItem--displayed:nth-child(2) .btn-plus")).Click(); + s.Driver.FindElement(By.CssSelector("#ItemsListOffcanvas button[data-bs-dismiss=\"offcanvas\"]")).Click(); + + s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click(); + s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click(); + s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click(); + Assert.Contains("4,23", s.Driver.FindElement(By.Id("Amount")).Text); + Assert.Contains("2 x Green Tea (1,00 €) = 2,00 € + 1 x Black Tea (1,00 €) = 1,00 € + 1,23 €", s.Driver.FindElement(By.Id("Calculation")).Text); + + // Pay + s.Driver.FindElement(By.Id("pay-button")).Click(); + s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); + s.Driver.FindElement(By.Id("DetailsToggle")).Click(); + s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat")); + Assert.Contains("4,23 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + s.PayInvoice(true); + TestUtils.Eventually(() => + { + s.MineBlockOnInvoiceCheckout(); + Assert.True(s.Driver.WaitForElement(By.Id("settled")).Displayed); + }); + + // Receipt + s.Driver.WaitForElement(By.Id("ReceiptLink")).Click(); + + additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table")); + items = additionalData.FindElements(By.CssSelector("tbody tr")); + sums = additionalData.FindElements(By.CssSelector("tfoot tr")); + Assert.Equal(3, items.Count); + Assert.Equal(2, sums.Count); + Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text); + Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text); + Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Total", sums[1].FindElement(By.CssSelector("th")).Text); + Assert.Contains("4,23 €", sums[1].FindElement(By.CssSelector("td")).Text); // Guest user can access recent transactions s.GoToHome(); @@ -2653,24 +2714,27 @@ namespace BTCPayServer.Tests [Fact] [Trait("Selenium", "Selenium")] - [Trait("Lightning", "Lightning")] public async Task CanUsePOSCart() { using var s = CreateSeleniumTester(); - s.Server.ActivateLightning(); await s.StartAsync(); - await s.Server.EnsureChannelsSetup(); - + + // Create users + var user = s.RegisterNewUser(); + var userAccount = s.AsTestAccount(); + s.GoToHome(); + s.Logout(); + s.GoToRegister(); s.RegisterNewUser(true); - s.CreateNewStore(); + + // Setup store and associate user + (_, string storeId) = s.CreateNewStore(); s.GoToStore(); - s.AddLightningNode(LightningConnectionType.CLightning, false); - - var appName = $"PoS-{Guid.NewGuid().ToString()[..21]}"; - s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click(); - s.Driver.FindElement(By.Id("AppName")).SendKeys(appName); - s.Driver.FindElement(By.Id("Create")).Click(); - Assert.Contains("App successfully created", s.FindAlertMessage().Text); + s.AddDerivationScheme(); + s.AddUserToStore(storeId, user, "Guest"); + + // Setup POS + s.CreateApp("PointOfSale"); s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click(); s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR"); Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected); @@ -2683,6 +2747,8 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("ShowDiscount")).Click(); s.Driver.FindElement(By.Id("SaveSettings")).Click(); Assert.Contains("App updated", s.FindAlertMessage().Text); + + // View s.Driver.FindElement(By.Id("ViewApp")).Click(); var windows = s.Driver.WindowHandles; Assert.Equal(2, windows.Count); @@ -2768,11 +2834,59 @@ namespace BTCPayServer.Tests Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); // Pay - s.PayInvoice(); + s.PayInvoice(true); + TestUtils.Eventually(() => + { + s.MineBlockOnInvoiceCheckout(); + Assert.True(s.Driver.WaitForElement(By.Id("settled")).Displayed); + }); + + // Receipt + s.Driver.WaitForElement(By.Id("ReceiptLink")).Click(); + var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table")); + var items = additionalData.FindElements(By.CssSelector("tbody tr")); + var sums = additionalData.FindElements(By.CssSelector("tfoot tr")); + Assert.Equal(7, items.Count); + Assert.Equal(4, sums.Count); + Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text); + Assert.Contains("2 x 1,00 € = 2,00 €", items[0].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 x 1,00 € = 1,00 €", items[1].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Rooibos (limited)", items[2].FindElement(By.CssSelector("th")).Text); + Assert.Contains("2 x 1,20 € = 2,40 €", items[2].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Herbal Tea (minimum) (1,80 €)", items[3].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 x 1,80 € = 1,80 €", items[3].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Herbal Tea (minimum) (2,30 €)", items[4].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 x 2,30 € = 2,30 €", items[4].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Fruit Tea (any amount) (0,20 €)", items[5].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 x 0,20 € = 0,20 €", items[5].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Fruit Tea (any amount) (0,30 €)", items[6].FindElement(By.CssSelector("th")).Text); + Assert.Contains("1 x 0,30 € = 0,30 €", items[6].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text); + Assert.Contains("10,00 €", sums[0].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Discount", sums[1].FindElement(By.CssSelector("th")).Text); + Assert.Contains("10% = 1,00 €", sums[1].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Tip", sums[2].FindElement(By.CssSelector("th")).Text); + Assert.Contains("10% = 0,90 €", sums[2].FindElement(By.CssSelector("td")).Text); + Assert.Contains("Total", sums[3].FindElement(By.CssSelector("th")).Text); + Assert.Contains("9,90 €", sums[3].FindElement(By.CssSelector("td")).Text); // Check inventory got updated and is now 3 instead of 5 s.Driver.Navigate().GoToUrl(posUrl); - Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text); + Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text); + + // Guest user can access recent transactions + s.GoToHome(); + s.Logout(); + s.LogIn(user, userAccount.RegisterDetails.Password); + s.GoToUrl(posUrl); + s.Driver.FindElement(By.Id("RecentTransactionsToggle")); + s.GoToHome(); + s.Logout(); + + // Unauthenticated user can't access recent transactions + s.GoToUrl(posUrl); + s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle")); } [Fact] diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index e5455f981..120f2ccf3 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2274,18 +2274,18 @@ namespace BTCPayServer.Tests }); // Test on the webhooks - user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, + await user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, c => { Assert.False(c.ManuallyMarked); Assert.True(c.OverPaid); }); - user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, + await user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, c => { Assert.True(c.OverPaid); }); - user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment, + await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment, c => { Assert.False(c.AfterExpiration); @@ -2295,7 +2295,7 @@ namespace BTCPayServer.Tests Assert.StartsWith(txId.ToString(), c.Payment.Id); }); - user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled, + await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled, c => { Assert.False(c.AfterExpiration); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index 2a4111e02..e7b76dfbb 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -274,6 +274,7 @@ namespace BTCPayServer.Controllers.Greenfield { Title = request.Title ?? request.AppName, DefaultView = (PosViewType)request.DefaultView, + ShowItems = request.ShowItems, ShowCustomAmount = request.ShowCustomAmount, ShowDiscount = request.ShowDiscount, ShowSearch = request.ShowSearch, @@ -335,6 +336,7 @@ namespace BTCPayServer.Controllers.Greenfield Created = appData.Created, Title = settings.Title, DefaultView = settings.DefaultView.ToString(), + ShowItems = settings.ShowItems, ShowCustomAmount = settings.ShowCustomAmount, ShowDiscount = settings.ShowDiscount, ShowSearch = settings.ShowSearch, diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs index 6126d0776..57b0b27a5 100644 --- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs +++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs @@ -101,6 +101,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers StoreBranding = storeBranding, Step = step.ToString(CultureInfo.InvariantCulture), ViewType = (PosViewType)viewType, + ShowItems = settings.ShowItems, ShowCustomAmount = settings.ShowCustomAmount, ShowDiscount = settings.ShowDiscount, ShowSearch = settings.ShowSearch, @@ -216,9 +217,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers title = settings.Title; // if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items price = amount; - if (currentView == PosViewType.Cart && AppService.TryParsePosCartItems(jposData, out cartItems)) + if (AppService.TryParsePosCartItems(jposData, out cartItems)) { - price = 0.0m; + price = jposData.TryGetValue("amounts", out var amounts) && amounts is JArray { Count: > 0 } amountsArray + ? amountsArray.Values().Sum() + : 0.0m; choices = AppService.Parse(settings.Template, false); foreach (var cartItem in cartItems) { @@ -379,6 +382,14 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})"; cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}"); } + + if (jposData.TryGetValue("amounts", out var amounts) && amounts is JArray { Count: > 0 } amountsArray) + { + for (var i = 0; i < amountsArray.Count; i++) + { + cartData.Add($"Manual entry {i+1}", _displayFormatter.Currency(amountsArray[i].ToObject(), settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)); + } + } receiptData.Add("Cart", cartData); } receiptData.Add("Subtotal", _displayFormatter.Currency(appPosData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)); @@ -580,6 +591,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers AppName = app.Name, Title = settings.Title, DefaultView = settings.DefaultView, + ShowItems = settings.ShowItems, ShowCustomAmount = settings.ShowCustomAmount, ShowDiscount = settings.ShowDiscount, ShowSearch = settings.ShowSearch, @@ -670,6 +682,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers { Title = vm.Title, DefaultView = vm.DefaultView, + ShowItems = vm.ShowItems, ShowCustomAmount = vm.ShowCustomAmount, ShowDiscount = vm.ShowDiscount, ShowSearch = vm.ShowSearch, diff --git a/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs index 95f9cc56d..786535ac2 100644 --- a/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs +++ b/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs @@ -27,6 +27,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models [Display(Name = "Point of Sale Style")] public PosViewType DefaultView { get; set; } + [Display(Name = "Display item selection for keypad")] + public bool ShowItems { get; set; } [Display(Name = "User can input custom amount")] public bool ShowCustomAmount { get; set; } [Display(Name = "User can input discount in %")] diff --git a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs index 57954cfb5..dd241fe6e 100644 --- a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs @@ -62,6 +62,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models public string StoreName { get; set; } public CurrencyInfoData CurrencyInfo { get; set; } public PosViewType ViewType { get; set; } + public bool ShowItems { get; set; } public bool ShowCustomAmount { get; set; } public bool ShowDiscount { get; set; } public bool ShowSearch { get; set; } = true; diff --git a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs index 89dc1251e..3acebc45e 100644 --- a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs +++ b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs @@ -87,6 +87,7 @@ namespace BTCPayServer.Services.Apps public string Template { get; set; } public bool EnableShoppingCart { get; set; } public PosViewType DefaultView { get; set; } + public bool ShowItems { get; set; } public bool ShowCustomAmount { get; set; } public bool ShowDiscount { get; set; } public bool ShowSearch { get; set; } = true; diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml index 066f09452..162f80fdc 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml @@ -1,6 +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 @inject DisplayFormatter DisplayFormatter @inject BTCPayServer.Security.ContentSecurityPolicies Csp @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @@ -37,6 +40,9 @@ + @if (Model.ShowSearch) { @@ -64,7 +70,6 @@ @for (var index = 0; index < Model.Items.Length; index++) { var item = Model.Items[index]; - var formatted = GetItemPriceFormatted(item); var inStock = item.Inventory is null or > 0; var buttonText = string.IsNullOrEmpty(item.BuyButtonText) @@ -73,7 +78,7 @@ buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted); var categories = new JArray(item.Categories ?? new object[] { });
-
+
@if (!string.IsNullOrWhiteSpace(item.Image)) { @item.Title @@ -133,7 +138,7 @@

Cart

-
+
diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/RecentTransactions.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/RecentTransactions.cshtml new file mode 100644 index 000000000..50a29e09d --- /dev/null +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/RecentTransactions.cshtml @@ -0,0 +1,36 @@ +@using BTCPayServer.Client +@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel + + + diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml index fa4d31187..26de7a69a 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml @@ -1,9 +1,24 @@ +@using BTCPayServer.Plugins.PointOfSale.Models +@using BTCPayServer.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using Newtonsoft.Json.Linq @using BTCPayServer.Client +@inject DisplayFormatter DisplayFormatter @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel - +@functions { + private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item) + { + if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.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; + } +}
- - + + + +
{{currencyCode}}
{{ formatCurrency(total, false) }}
@@ -58,39 +73,84 @@
-